Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
e5fbae3
Initial commit of uniflight
schgoo Dec 10, 2025
9eb29c0
Update docs
schgoo Dec 10, 2025
dc9c254
Merge branch 'main' into uniflight
schgoo Dec 10, 2025
e29c3f4
Fix clippy
schgoo Dec 10, 2025
eac8971
Merge branch 'uniflight' of https://github.com/schgoo/oxidizer into u…
schgoo Dec 10, 2025
3f508a5
Add logo
schgoo Dec 11, 2025
b692e0e
Merge with main
schgoo Dec 11, 2025
ff628d0
Fix formatting
schgoo Dec 11, 2025
8aa3ffb
Update favicon and code coverage
schgoo Dec 11, 2025
6ce6e11
Update crates/uniflight/Cargo.toml
schgoo Dec 11, 2025
76e0ede
Update crates/uniflight/Cargo.toml
schgoo Dec 11, 2025
e6e7458
Move tests to separate file
schgoo Dec 16, 2025
1ca1d3e
Merge with main
schgoo Dec 16, 2025
bf25a1a
Use tick
schgoo Dec 16, 2025
e9697fa
Add N leaders functionality (for redundancy)
schgoo Dec 16, 2025
1aa66c4
Merge branch 'uniflight' of https://github.com/schgoo/oxidizer into u…
schgoo Dec 16, 2025
af7f96c
Refactoring to allow followers to unlock in parallel
schgoo Dec 17, 2025
9445788
Update to use async-once and dashmap. Update benchmarks
schgoo Jan 7, 2026
0837809
Merge with main
schgoo Jan 7, 2026
8efdde2
Fix comments
schgoo Jan 13, 2026
9ef7039
Make it thread_aware
schgoo Jan 13, 2026
85c5770
Merge with main
schgoo Jan 13, 2026
aeb1cb1
Fix merge issue
schgoo Jan 13, 2026
4cc0a76
Fix external types check
schgoo Jan 13, 2026
74cbc11
Fix pr issues
schgoo Jan 13, 2026
da0eebe
Fix pr issues
schgoo Jan 13, 2026
e0f88e5
cargo fmt
schgoo Jan 14, 2026
575baf0
Fix clippy issues
schgoo Jan 14, 2026
e60a4df
Merge branch 'main' into uniflight
schgoo Jan 14, 2026
beaacdb
Fix PR issues
schgoo Jan 14, 2026
4b81fe3
Merge branch 'uniflight' of https://github.com/schgoo/oxidizer into u…
schgoo Jan 14, 2026
bb97d17
Remove excessive comments. Improve code coverage
schgoo Jan 15, 2026
693d224
Use tick::Clock::delay instead of tokio::time::sleep
schgoo Jan 15, 2026
dd3bd07
Merge branch 'main' into uniflight
schgoo Jan 15, 2026
fcde8fa
Merge branch 'uniflight' of https://github.com/schgoo/oxidizer into u…
schgoo Jan 15, 2026
e6b76f5
clippy
schgoo Jan 15, 2026
7f28cfe
Rename work to execute. Remove unnecessary attribution
schgoo Jan 15, 2026
61f8ce6
better logo
schgoo Jan 16, 2026
455c7d2
Merge with main
schgoo Jan 20, 2026
dd104ea
Update readme
schgoo Jan 20, 2026
5345752
Undo ToC changes to README.md
schgoo Jan 21, 2026
ab92d59
Improve benchmarks for incremental development. Remove thread_aware e…
schgoo Jan 22, 2026
f19b674
Update readme
schgoo Jan 22, 2026
168acbb
Add thread aware types to external type check
schgoo Jan 22, 2026
3792000
Clippy, fmt
schgoo Jan 22, 2026
80d0766
Merge branch 'main' into uniflight
schgoo Jan 22, 2026
ae9ce04
Clippy, readme
schgoo Jan 23, 2026
d6dd361
Merge branch 'main' of https://github.com/microsoft/oxidizer into uni…
schgoo Jan 23, 2026
cb49236
Merge branch 'uniflight' of https://github.com/schgoo/oxidizer into u…
schgoo Jan 23, 2026
321c353
Clippy, readme, surface task panics back to caller and all followers …
schgoo Jan 28, 2026
760479c
Merge with main
schgoo Jan 28, 2026
75cea13
CI failures
schgoo Jan 28, 2026
828fc07
Merge with main
schgoo Jan 29, 2026
a4019d5
Merge branch 'main' into uniflight
schgoo Jan 29, 2026
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
5 changes: 5 additions & 0 deletions .spelling
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
304
0.X.Y
100k
Expand Down Expand Up @@ -82,6 +83,8 @@ C-SERDE
C-SMART-PTR
deallocate
Debuggability
Deduplicate
deduplicating
deduplication
deque
Deque
Expand Down Expand Up @@ -279,6 +282,8 @@ unconfigured
uncontended
unhandleable
unicode
uniflight
uniflight's
Uninit
unordered
unredacted
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,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)
62 changes: 61 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 @@ -43,13 +43,18 @@ thread_aware = { path = "crates/thread_aware", default-features = false, version
thread_aware_macros = { path = "crates/thread_aware_macros", default-features = false, version = "0.6.1" }
thread_aware_macros_impl = { path = "crates/thread_aware_macros_impl", default-features = false, version = "0.6.1" }
tick = { path = "crates/tick", default-features = false, version = "0.1.2" }
uniflight = { path = "crates/uniflight", default-features = false, version = "0.1.0" }

# external dependencies
ahash = { version = "0.8", default-features = false }
alloc_tracker = { version = "0.5.9", default-features = false }
anyhow = { version = "1.0.100", default-features = false }
async-once-cell = { version = "0.5", 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.8.1", 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 }
dynosaur = { version = "0.3.0", default-features = false }
Expand All @@ -73,6 +78,7 @@ once_cell = { version = "1.21.3", default-features = false }
opentelemetry = { version = "0.31.0", default-features = false }
opentelemetry-stdout = { version = "0.31.0", default-features = false }
opentelemetry_sdk = { version = "0.31.0", 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 Down Expand Up @@ -101,6 +107,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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ These are the primary crates built out of this repo:
- [`seatbelt`](./crates/seatbelt/README.md) - Resilience and recovery mechanisms for fallible operations.
- [`thread_aware`](./crates/thread_aware/README.md) - Facilities to support thread-isolated state.
- [`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

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

[package]
name = "uniflight"
description = "Coalesces duplicate async tasks into a single execution."
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

[package.metadata.cargo_check_external_types]
allowed_external_types = [
"thread_aware::cell::builtin::PerProcess",
Comment thread
geeknoid marked this conversation as resolved.
"thread_aware::cell::storage::Strategy",
"thread_aware::core::ThreadAware",
]

[dependencies]
ahash = { workspace = true, default-features = false, features = ["std"] }
async-once-cell.workspace = true
dashmap.workspace = true
futures-util = { workspace = true, default-features = false, features = ["std", "alloc"] }
thread_aware.workspace = true

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

[lints]
workspace = true

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

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

# 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/actions/workflows/main.yml/badge.svg?event=push)](https://github.com/microsoft/oxidizer/actions/workflows/main.yml)
[![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)
<a href="../.."><img src="../../logo.svg" alt="This crate was developed as part of the Oxidizer project" width="20"></a>

</div>

Coalesces duplicate async tasks into a single execution.

This crate provides [`Merger`][__link0], 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<String, String> = Merger::new();

// Multiple concurrent calls with the same key will share a single execution.
// Note: you can pass &str directly when the key type is String.
let result = group.execute("user:123", || async {
// This expensive operation runs only once, even if called concurrently
"expensive_result".to_string()
}).await.expect("leader should not panic");
```

## Flexible Key Types

The [`Merger::execute`][__link1] method accepts keys using [`Borrow`][__link2] semantics, allowing you to pass
borrowed forms of the key type. For example, with `Merger<String, T>`, you can pass `&str`
directly without allocating:

```rust
let merger: Merger<String, i32> = Merger::new();

// Pass &str directly - no need to call .to_string()
let result = merger.execute("my-key", || async { 42 }).await;
assert_eq!(result, Ok(42));
```

## Thread-Aware Scoping

`Merger` supports thread-aware scoping via a [`Strategy`][__link3]
type parameter. This controls how the internal state is partitioned across threads/NUMA nodes:

* [`PerProcess`][__link4] (default): Single global state, maximum deduplication
* [`PerNuma`][__link5]: Separate state per NUMA node, NUMA-local memory access
* [`PerCore`][__link6]: Separate state per core, no deduplication (useful for already-partitioned work)

```rust
use uniflight::Merger;
use thread_aware::PerNuma;

// NUMA-aware merger - each NUMA node gets its own deduplication scope
let merger: Merger<String, String, PerNuma> = Merger::new_per_numa();
```

## Cancellation and Panic Handling

`Merger` handles task cancellation and panics explicitly:

* If the leader task is cancelled or dropped, a follower becomes the new leader
* If the leader task panics, followers receive [`LeaderPanicked`][__link7] error with the panic message
* Followers that join before the leader completes receive the value the leader returns

When a panic occurs, followers are notified via the error type rather than silently
retrying. The panic message is captured and available via [`LeaderPanicked::message`][__link8]:

```rust
let merger: Merger<String, String> = Merger::new();
match merger.execute("key", || async { "result".to_string() }).await {
Ok(value) => println!("got {value}"),
Err(err) => {
println!("leader panicked: {}", err.message());
// Decide whether to retry
}
}
```

## Memory Management

Completed entries are automatically removed from the internal map when the last caller
finishes. This ensures no stale entries accumulate over time.

## Type Requirements

The value type `T` must implement [`Clone`][__link9] because followers receive a clone of the
leader’s result. The key type `K` must implement [`Hash`][__link10] and [`Eq`][__link11].

## Thread Safety

[`Merger`][__link12] 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

Run benchmarks with `cargo bench -p uniflight`. The suite covers:

* `single_call`: Baseline latency with no contention
* `high_contention_100`: 100 concurrent tasks on the same key
* `distributed_10x10`: 10 keys with 10 tasks each

Use `--save-baseline` and `--baseline` flags to track regressions over time.


<hr/>
<sub>
This crate was developed as part of <a href="../..">The Oxidizer Project</a>. Browse this crate's <a href="https://github.com/microsoft/oxidizer/tree/main/crates/uniflight">source code</a>.
</sub>

[__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEGxgwNFq9VUtfG5xaBNm6U4VGG97W2YkyKkPjG4KVgSbTgdOrYWSCgmx0aHJlYWRfYXdhcmVlMC42LjGCaXVuaWZsaWdodGUwLjEuMA
[__link0]: https://docs.rs/uniflight/0.1.0/uniflight/struct.Merger.html
[__link1]: https://docs.rs/uniflight/0.1.0/uniflight/?search=Merger::execute
[__link10]: https://doc.rust-lang.org/stable/std/?search=hash::Hash
[__link11]: https://doc.rust-lang.org/stable/std/cmp/trait.Eq.html
[__link12]: https://docs.rs/uniflight/0.1.0/uniflight/struct.Merger.html
[__link2]: https://doc.rust-lang.org/stable/std/?search=borrow::Borrow
[__link3]: https://docs.rs/thread_aware/0.6.1/thread_aware/?search=storage::Strategy
[__link4]: https://docs.rs/thread_aware/0.6.1/thread_aware/?search=PerProcess
[__link5]: https://docs.rs/thread_aware/0.6.1/thread_aware/?search=PerNuma
[__link6]: https://docs.rs/thread_aware/0.6.1/thread_aware/?search=PerCore
[__link7]: https://docs.rs/uniflight/0.1.0/uniflight/struct.LeaderPanicked.html
[__link8]: https://docs.rs/uniflight/0.1.0/uniflight/?search=LeaderPanicked::message
[__link9]: https://doc.rust-lang.org/stable/std/clone/trait.Clone.html
Loading
Loading