Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,5 @@ _manifest
ARROW

# Agent files
.claude
.claude
CLAUDE.md
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

Please see each crate's change log below:

- [`async_once`](./crates/async_once/CHANGELOG.md)
- [`bytesbuf`](./crates/bytesbuf/CHANGELOG.md)
- [`bytesbuf_io`](./crates/bytesbuf_io/CHANGELOG.md)
- [`data_privacy`](./crates/data_privacy/CHANGELOG.md)
Expand All @@ -16,3 +17,4 @@ Please see each crate's change log below:
- [`thread_aware_macros`](./crates/thread_aware_macros/CHANGELOG.md)
- [`thread_aware_macros_impl`](./crates/thread_aware_macros_impl/CHANGELOG.md)
- [`tick`](./crates/tick/CHANGELOG.md)
- [`uniflight`](./crates/uniflight/CHANGELOG.md)
59 changes: 58 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,20 @@ thread_aware = { path = "crates/thread_aware", default-features = false, version
thread_aware_macros = { path = "crates/thread_aware_macros", default-features = false, version = "0.6.0" }
thread_aware_macros_impl = { path = "crates/thread_aware_macros_impl", default-features = false, version = "0.6.0" }
tick = { path = "crates/tick", default-features = false, version = "0.1.2" }
uniflight = { path = "crates/uniflight", default-features = false, version = "0.1.0" }

# external dependencies
alloc_tracker = { version = "0.5.9", default-features = false }
async-once-cell = { version = "0.5", default-features = false }
anyhow = { version = "1.0.100", default-features = false }
bytes = { version = "1.11.0", default-features = false }
chrono = { version = "0.4.40", default-features = false }
chrono-tz = { version = "0.10.4", default-features = false }
criterion = { version = "0.7.0", default-features = false }
dashmap = { version = "6.1", default-features = false }
derive_more = { version = "2.0.1", default-features = false }
duct = { version = "1.1.1", default-features = false }
event-listener = { version = "5.4.0", default-features = false }
futures = { version = "0.3.31", default-features = false }
futures-core = { version = "0.3.31", default-features = false }
futures-util = { version = "0.3.31", default-features = false }
Expand All @@ -63,6 +67,7 @@ new_zealand = { version = "1.0.1", default-features = false }
nm = { version = "0.1.21", default-features = false }
num-traits = { version = "0.2.19", default-features = false }
once_cell = { version = "1.21.3", default-features = false }
parking_lot = { version = "0.12.5", default-features = false }
pin-project-lite = { version = "0.2.13", default-features = false }
pretty_assertions = { version = "1.4.1", default-features = false }
prettyplease = { version = "0.2.37", default-features = false }
Expand All @@ -75,6 +80,7 @@ rustc-hash = { version = "2.1.0", default-features = false }
serde = { version = "1.0.228", default-features = false }
serde_core = { version = "1.0.228", default-features = false }
serde_json = { version = "1.0.145", default-features = false }
singleflight-async = { version = "0.2", default-features = false }
smallvec = { version = "1.15.1", default-features = false }
static_assertions = { version = "1.1.0", default-features = false }
syn = { version = "2.0.111", default-features = false }
Expand All @@ -88,6 +94,7 @@ trait-variant = { version = "0.1.2", default-features = false }
trybuild = { version = "1.0.114", default-features = false }
typeid = { version = "1.0.3", default-features = false }
windows-sys = { version = "0.61.2", default-features = false }
xutex = { version = "0.2.0", default-features = false }
xxhash-rust = { version = "0.8.15", default-features = false }

[workspace.lints.rust]
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,15 @@

This repository contains a set of crates that help you build robust highly scalable services in Rust.

- [Crates](#crates)
- [About this Repo](#about-this-repo)
- [The Oxidizer Project](#the-oxidizer-project)
- [Crates](#crates)
- [About this Repo](#about-this-repo)
- [Adding New Crates](#adding-new-crates)
- [Publishing Crates](#publishing-crates)
- [Documenting Crates](#documenting-crates)
- [CI Workflows](#ci-workflows)
- [Pull Request Gates](#pull-request-gates)
- [Trademarks](#trademarks)
- [Trademarks](#trademarks)

## Crates

Expand All @@ -38,6 +39,7 @@ These are the crates built out of this repo:
- [`thread_aware_macros`](./crates/thread_aware_macros/README.md) - Macros for the `thread_aware` crate.
- [`thread_aware_macros_impl`](./crates/thread_aware_macros_impl/README.md) - Macros for the `thread_aware` crate.
- [`tick`](./crates/tick/README.md) - Provides primitives to interact with and manipulate machine time.
- [`uniflight`](./crates/uniflight/README.md) - Coalesces duplicate async tasks into a single execution.

## About this Repo

Expand Down
8 changes: 8 additions & 0 deletions crates/uniflight/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changelog

## [0.1.0] - 2025-12-10

- 🧩 Miscellaneous

- Initial commit of uniflight

43 changes: 43 additions & 0 deletions crates/uniflight/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

[package]
name = "uniflight"
description = "Coalesces multiple ongoing tasks into a leader which does the work, and follower tasks that wait on the result, to prevent duplicate I/O or other downstream overhead."
version = "0.1.0"
readme = "README.md"
keywords = ["oxidizer", "coalescing", "stempede", "singleflight", "deduplication"]
categories = ["concurrency"]

edition.workspace = true
rust-version.workspace = true
authors.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true

[dependencies]
async-once-cell.workspace = true
dashmap.workspace = true

[dev-dependencies]
criterion = { workspace = true, features = ["async_tokio"] }
futures-util = { workspace = true, features = ["alloc", "std"] }
singleflight-async.workspace = true
tick = { workspace = true, features = ["tokio"] }
tokio = { workspace = true, features = [
"macros",
"rt",
"time",
"rt-multi-thread",
] }

[lints]
workspace = true

[[bench]]
name = "comparison"
harness = false

[[example]]
name = "cache_population"
83 changes: 83 additions & 0 deletions crates/uniflight/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<div align="center">
<img src="./logo.png" alt="Uniflight Logo" width="128">

# Uniflight

[![crate.io](https://img.shields.io/crates/v/uniflight.svg)](https://crates.io/crates/uniflight)
[![docs.rs](https://docs.rs/uniflight/badge.svg)](https://docs.rs/uniflight)
[![MSRV](https://img.shields.io/crates/msrv/uniflight)](https://crates.io/crates/uniflight)
[![CI](https://github.com/microsoft/oxidizer/workflows/main/badge.svg)](https://github.com/microsoft/oxidizer/actions)
[![Coverage](https://codecov.io/gh/microsoft/oxidizer/graph/badge.svg?token=FCUG0EL5TI)](https://codecov.io/gh/microsoft/oxidizer)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](../LICENSE)

</div>

* [Summary](#summary)

## Summary

<!-- cargo-rdme start -->

Coalesces duplicate async tasks into a single execution.

This crate provides [`Merger`], a mechanism for deduplicating concurrent async operations.
When multiple tasks request the same work (identified by a key), only the first task (the
"leader") performs the actual work while subsequent tasks (the "followers") wait and receive
a clone of the result.

## When to Use

Use `Merger` when you have expensive or rate-limited operations that may be requested
concurrently with the same parameters:

- **Cache population**: Prevent thundering herd when a cache entry expires
- **API calls**: Deduplicate concurrent requests to the same endpoint
- **Database queries**: Coalesce identical queries issued simultaneously
- **File I/O**: Avoid reading the same file multiple times concurrently

## Example

```rust
use uniflight::Merger;

let group: Merger<&str, String> = Merger::new();

// Multiple concurrent calls with the same key will share a single execution
let result = group.work("user:123", || async {
// This expensive operation runs only once, even if called concurrently
"expensive_result".to_string()
}).await;
```

## Cancellation and Panic Safety

`Merger` handles task cancellation and panics gracefully:

- If the leader task is cancelled or dropped, a follower becomes the new leader
- If the leader task panics, a follower becomes the new leader and executes its work
- Followers that join before the leader completes receive the cached result

## Thread Safety

[`Merger`] is `Send` and `Sync`, and can be shared across threads. The returned futures
are `Send` when the closure, future, key, and value types are `Send`.

## Performance

Benchmarks comparing `uniflight` against `singleflight-async` show the following characteristics:

- **Concurrent workloads** (10+ tasks): uniflight is 1.2-1.3x faster, demonstrating better scalability under contention
- **Single calls**: singleflight-async has lower per-call overhead (~2x faster for individual operations)
- **Multiple keys**: uniflight performs 1.3x faster when handling multiple distinct keys concurrently

uniflight's DashMap-based architecture provides excellent scaling properties for high-concurrency scenarios,
making it well-suited for production workloads with concurrent access patterns. For low-contention scenarios
with predominantly single calls, the performance difference is minimal (sub-microsecond range).

<!-- cargo-rdme end -->

<div style="font-size: 75%" ><hr/>

This crate was developed as part of [The Oxidizer Project](https://github.com/microsoft/oxidizer).

</div>
Loading
Loading