diff --git a/.gitignore b/.gitignore
index 20b91f6b..6368ac41 100644
--- a/.gitignore
+++ b/.gitignore
@@ -35,4 +35,5 @@ _manifest
ARROW
# Agent files
-.claude
\ No newline at end of file
+.claude
+CLAUDE.md
\ No newline at end of file
diff --git a/.spelling b/.spelling
index e9ceb2a8..c17aa392 100644
--- a/.spelling
+++ b/.spelling
@@ -1,3 +1,4 @@
+304
→
0.X.Y
100k
@@ -37,6 +38,7 @@ btree_map
buildable
bytesbuf
callee
+cancelled
Cargo.toml
C-BITFLAG
C-CONV
@@ -81,6 +83,8 @@ C-SERDE
C-SMART-PTR
deallocate
Debuggability
+Deduplicate
+deduplicating
deduplication
deque
Deque
@@ -273,6 +277,8 @@ unconfigured
uncontended
unhandleable
unicode
+uniflight
+uniflight's
Uninit
unordered
unredacted
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9862cfa6..64a2fc85 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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)
@@ -18,3 +19,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)
diff --git a/Cargo.lock b/Cargo.lock
index 0022259b..4239b9ba 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "alloc_tracker"
-version = "0.5.12"
+version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "541f3dfa28da9fba29d46a58d1fc137d401b7cdd5fff76c2cdc03abe3ad5d87c"
+checksum = "6d6f8b0cbd2617bdb93d237d7e11032f0ec3ea509ce1fbd351397567e36dd6d6"
[[package]]
name = "android_system_properties"
@@ -38,6 +38,12 @@ version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
+[[package]]
+name = "async-once-cell"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288f83726785267c6f2ef073a3d83dc3f9b81464e9f99898240cced85fce35a"
+
[[package]]
name = "autocfg"
version = "1.5.0"
@@ -119,9 +125,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
-version = "1.2.51"
+version = "1.2.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
+checksum = "755d2fce177175ffca841e9a06afdb2c4ab0f593d53b4dee48147dfaade85932"
dependencies = [
"find-msvc-tools",
"shlex",
@@ -135,9 +141,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "chrono"
-version = "0.4.42"
+version = "0.4.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
+checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118"
dependencies = [
"iana-time-zone",
"num-traits",
@@ -183,18 +189,18 @@ dependencies = [
[[package]]
name = "clap"
-version = "4.5.53"
+version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9e340e012a1bf4935f5282ed1436d1489548e8f72308207ea5df0e23d2d03f8"
+checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
-version = "4.5.53"
+version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d76b5d13eaa18c901fd2f7fca939fefe3a0727a953561fefdf3b2922b8569d00"
+checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
dependencies = [
"anstyle",
"clap_lex",
@@ -202,9 +208,9 @@ dependencies = [
[[package]]
name = "clap_lex"
-version = "0.7.6"
+version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
+checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32"
[[package]]
name = "convert_case"
@@ -223,13 +229,13 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpulist"
-version = "1.0.2"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "11a45acbbf4646bcff1e38cfd1f7d88ae73353d4add7ca31609023216599bbfa"
+checksum = "10921f832bb6e2075c6910ab0be7b33e413029ae6d74b01f049fc6cb237362e6"
dependencies = [
"itertools 0.14.0",
"new_zealand",
- "thiserror 2.0.17",
+ "thiserror 2.0.18",
]
[[package]]
@@ -250,6 +256,7 @@ dependencies = [
"serde",
"serde_json",
"tinytemplate",
+ "tokio",
"walkdir",
]
@@ -263,12 +270,32 @@ dependencies = [
"itertools 0.13.0",
]
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
[[package]]
name = "crunchy"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
+[[package]]
+name = "dashmap"
+version = "6.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf"
+dependencies = [
+ "cfg-if",
+ "crossbeam-utils",
+ "hashbrown 0.14.5",
+ "lock_api",
+ "once_cell",
+ "parking_lot_core",
+]
+
[[package]]
name = "data_privacy"
version = "0.10.1"
@@ -405,9 +432,9 @@ checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "find-msvc-tools"
-version = "0.1.6"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
+checksum = "8591b0bcc8a98a64310a2fae1bb3e9b8564dd10e381e6e28010fde8e8e8568db"
[[package]]
name = "foldhash"
@@ -417,9 +444,9 @@ checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "folo_ffi"
-version = "0.1.8"
+version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44b0401f7b448a28be3e723e6a30cd29ebe707ebbf23ac01ab505da0de10005c"
+checksum = "b2c4a9efbd06c6c8500f5d97a20159996679088bdb5f598a863960bedf3f2574"
[[package]]
name = "fragile"
@@ -582,6 +609,12 @@ dependencies = [
"byteorder",
]
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+
[[package]]
name = "hashbrown"
version = "0.16.1"
@@ -624,19 +657,19 @@ dependencies = [
[[package]]
name = "indexmap"
-version = "2.12.1"
+version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
+checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [
"equivalent",
- "hashbrown",
+ "hashbrown 0.16.1",
]
[[package]]
name = "infinity_pool"
-version = "0.8.4"
+version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6f81221d9c547a962679935ef10f3be03739f8a99c98ad7bfc1fcbd4850892bf"
+checksum = "1b53f0294af2d25dc8eee1d483579ad476233efe0ce1e09c1721275a976d7055"
dependencies = [
"new_zealand",
"num-integer",
@@ -646,9 +679,9 @@ dependencies = [
[[package]]
name = "insta"
-version = "1.45.1"
+version = "1.46.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "983e3b24350c84ab8a65151f537d67afbbf7153bb9f1110e03e9fa9b07f67a5c"
+checksum = "248b42847813a1550dafd15296fd9748c651d0c32194559dbc05d804d54b21e8"
dependencies = [
"once_cell",
"serde",
@@ -682,9 +715,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "jiff"
-version = "0.2.17"
+version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8"
+checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
@@ -697,9 +730,9 @@ dependencies = [
[[package]]
name = "jiff-static"
-version = "0.2.17"
+version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58"
+checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78"
dependencies = [
"proc-macro2",
"quote",
@@ -723,9 +756,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.83"
+version = "0.3.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8"
+checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
dependencies = [
"once_cell",
"wasm-bindgen",
@@ -756,9 +789,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "libc"
-version = "0.2.178"
+version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
+checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]]
name = "linux-raw-sys"
@@ -783,9 +816,9 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "many_cpus"
-version = "1.1.3"
+version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb82f238dc324f74bb9c2e25b4244dbdeab8d66809dc3e988472498e84a411ec"
+checksum = "d4ab614fc8e14ffa2aab66ed7942362b33191752f3a5bf9533e9f88e4204545f"
dependencies = [
"cpulist",
"derive_more",
@@ -853,15 +886,15 @@ dependencies = [
[[package]]
name = "new_zealand"
-version = "1.0.2"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62cc3bfd31ca5136b8449b5418a91a9c2a336b3ea61ae17eae350c10bd4ab26d"
+checksum = "e098f45dda35490b475350685c55a9fb4c4f6cb885f6f0b09fea301cfef52161"
[[package]]
name = "nm"
-version = "0.1.22"
+version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19e593410e637cfb2542a21f30106fde38fcf7bb8359b39c1355d382ad09f888"
+checksum = "93ee800dec1976af03cfa4c209e0fac85e140266782719f06bddfd321177acf9"
dependencies = [
"foldhash",
"new_zealand",
@@ -915,7 +948,7 @@ dependencies = [
"mutants",
"ohno_macros",
"regex",
- "thiserror 2.0.17",
+ "thiserror 2.0.18",
"tokio",
"typeid",
]
@@ -1128,18 +1161,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
-version = "1.0.104"
+version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
+checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
-version = "1.0.42"
+version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
+checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [
"proc-macro2",
]
@@ -1166,7 +1199,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1"
dependencies = [
"rand_chacha",
- "rand_core 0.9.3",
+ "rand_core 0.9.5",
]
[[package]]
@@ -1176,7 +1209,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
- "rand_core 0.9.3",
+ "rand_core 0.9.5",
]
[[package]]
@@ -1187,18 +1220,18 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
[[package]]
name = "rand_core"
-version = "0.9.3"
+version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
dependencies = [
"getrandom",
]
[[package]]
name = "rapidhash"
-version = "4.2.0"
+version = "4.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2988730ee014541157f48ce4dcc603940e00915edc3c7f9a8d78092256bb2493"
+checksum = "5d8b5b858a440a0bc02625b62dd95131b9201aa9f69f411195dd4a7cfb1de3d7"
dependencies = [
"rustversion",
]
@@ -1348,9 +1381,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.148"
+version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da"
+checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [
"itoa",
"memchr",
@@ -1437,9 +1470,9 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "syn"
-version = "2.0.111"
+version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
+checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [
"proc-macro2",
"quote",
@@ -1508,11 +1541,11 @@ dependencies = [
[[package]]
name = "thiserror"
-version = "2.0.17"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
- "thiserror-impl 2.0.17",
+ "thiserror-impl 2.0.18",
]
[[package]]
@@ -1528,9 +1561,9 @@ dependencies = [
[[package]]
name = "thiserror-impl"
-version = "2.0.17"
+version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
dependencies = [
"proc-macro2",
"quote",
@@ -1600,9 +1633,9 @@ dependencies = [
[[package]]
name = "time"
-version = "0.3.44"
+version = "0.3.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d"
+checksum = "f9e442fc33d7fdb45aa9bfeb312c095964abdf596f7567261062b2a7107aaabd"
dependencies = [
"deranged",
"itoa",
@@ -1611,22 +1644,22 @@ dependencies = [
"num-conv",
"num_threads",
"powerfmt",
- "serde",
+ "serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
-version = "0.1.6"
+version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b"
+checksum = "8b36ee98fd31ec7426d599183e8fe26932a8dc1fb76ddb6214d05493377d34ca"
[[package]]
name = "time-macros"
-version = "0.2.24"
+version = "0.2.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3"
+checksum = "71e552d1249bf61ac2a52db88179fd0673def1e1ad8243a00d9ec9ed71fee3dd"
dependencies = [
"num-conv",
"time-core",
@@ -1663,9 +1696,9 @@ dependencies = [
[[package]]
name = "tokio"
-version = "1.48.0"
+version = "1.49.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
+checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86"
dependencies = [
"pin-project-lite",
"tokio-macros",
@@ -1684,9 +1717,9 @@ dependencies = [
[[package]]
name = "tokio-util"
-version = "0.7.17"
+version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
dependencies = [
"bytes",
"futures-core",
@@ -1697,9 +1730,9 @@ dependencies = [
[[package]]
name = "toml"
-version = "0.9.10+spec-1.1.0"
+version = "0.9.11+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48"
+checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
dependencies = [
"indexmap",
"serde_core",
@@ -1736,9 +1769,9 @@ checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
[[package]]
name = "tower"
-version = "0.5.2"
+version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
dependencies = [
"futures-core",
"futures-util",
@@ -1843,6 +1876,20 @@ version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+[[package]]
+name = "uniflight"
+version = "0.1.0"
+dependencies = [
+ "async-once-cell",
+ "criterion",
+ "dashmap",
+ "futures-util",
+ "mutants",
+ "thread_aware",
+ "tick",
+ "tokio",
+]
+
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -1855,18 +1902,18 @@ dependencies = [
[[package]]
name = "wasip2"
-version = "1.0.1+wasi-0.2.4"
+version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
+checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen"
-version = "0.2.106"
+version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd"
+checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
dependencies = [
"cfg-if",
"once_cell",
@@ -1877,9 +1924,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.106"
+version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3"
+checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -1887,9 +1934,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.106"
+version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40"
+checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -1900,9 +1947,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.106"
+version = "0.2.108"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4"
+checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
dependencies = [
"unicode-ident",
]
@@ -2160,9 +2207,9 @@ checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
[[package]]
name = "wit-bindgen"
-version = "0.46.0"
+version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
[[package]]
name = "xml-rs"
@@ -2178,18 +2225,18 @@ checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3"
[[package]]
name = "zerocopy"
-version = "0.8.31"
+version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3"
+checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.31"
+version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a"
+checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
dependencies = [
"proc-macro2",
"quote",
@@ -2198,6 +2245,6 @@ dependencies = [
[[package]]
name = "zmij"
-version = "1.0.3"
+version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e9747e91771f56fd7893e1164abd78febd14a670ceec257caad15e051de35f06"
+checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65"
diff --git a/Cargo.toml b/Cargo.toml
index 26dcf70c..d19271cc 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -41,13 +41,17 @@ 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
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.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 }
dynosaur = { version = "0.3.0", default-features = false }
@@ -65,6 +69,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 }
@@ -93,6 +98,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]
diff --git a/README.md b/README.md
index 0085af2e..0e17a57c 100644
--- a/README.md
+++ b/README.md
@@ -34,6 +34,7 @@ These are the primary crates built out of this repo:
- [`recoverable`](./crates/recoverable/README.md) - Recovery information and classification for resilience patterns.
- [`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
diff --git a/crates/uniflight/CHANGELOG.md b/crates/uniflight/CHANGELOG.md
new file mode 100644
index 00000000..0906fd27
--- /dev/null
+++ b/crates/uniflight/CHANGELOG.md
@@ -0,0 +1,8 @@
+# Changelog
+
+## [0.1.0] - 2025-12-10
+
+- 🧩 Miscellaneous
+
+ - Initial commit of uniflight
+
diff --git a/crates/uniflight/Cargo.toml b/crates/uniflight/Cargo.toml
new file mode 100644
index 00000000..29773101
--- /dev/null
+++ b/crates/uniflight/Cargo.toml
@@ -0,0 +1,51 @@
+# 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
+
+[package.metadata.cargo_check_external_types]
+allowed_external_types = [
+ "thread_aware::cell::builtin::PerProcess",
+ "thread_aware::cell::storage::Strategy",
+ "thread_aware::core::ThreadAware",
+]
+
+[dependencies]
+async-once-cell.workspace = true
+dashmap.workspace = true
+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"
diff --git a/crates/uniflight/README.md b/crates/uniflight/README.md
new file mode 100644
index 00000000..24829c1c
--- /dev/null
+++ b/crates/uniflight/README.md
@@ -0,0 +1,120 @@
+
+

+
+# Uniflight
+
+[](https://crates.io/crates/uniflight)
+[](https://docs.rs/uniflight)
+[](https://crates.io/crates/uniflight)
+[](https://github.com/microsoft/oxidizer/actions/workflows/main.yml)
+[](https://codecov.io/gh/microsoft/oxidizer)
+[](../../LICENSE)
+

+
+
+
+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 = 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;
+```
+
+## 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`, you can pass `&str`
+directly without allocating:
+
+```rust
+let merger: Merger = Merger::new();
+
+// Pass &str directly - no need to call .to_string()
+merger.execute("my-key", || async { 42 }).await;
+```
+
+## 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 = Merger::new_per_numa();
+```
+
+## 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
+
+## Memory Management
+
+Completed entries are automatically removed from the internal map when the last caller
+finishes. This ensures no stale entries accumulate over time.
+
+## Thread Safety
+
+[`Merger`][__link7] 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.
+
+
+
+
+This crate was developed as part of The Oxidizer Project. Browse this crate's source code.
+
+
+ [__cargo_doc2readme_dependencies_info]: ggGkYW0CYXSEGy4k8ldDFPOhG2VNeXtD5nnKG6EPY6OfW5wBG8g18NOFNdxpYXKEG7377t2ZpTeEGzAOjWIcwZYsG8xBz1ZJEGjgG3_fpjW3KImqYWSCgmx0aHJlYWRfYXdhcmVlMC42LjGCaXVuaWZsaWdodGUwLjEuMA
+ [__link0]: https://docs.rs/uniflight/0.1.0/uniflight/struct.Merger.html
+ [__link1]: https://docs.rs/uniflight/0.1.0/uniflight/?search=Merger::execute
+ [__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.Merger.html
diff --git a/crates/uniflight/benches/performance.rs b/crates/uniflight/benches/performance.rs
new file mode 100644
index 00000000..c96574c5
--- /dev/null
+++ b/crates/uniflight/benches/performance.rs
@@ -0,0 +1,99 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Performance benchmarks for uniflight.
+//!
+//! Run with: cargo bench -p uniflight
+//! Save baseline: cargo bench -p uniflight -- --save-baseline main
+//! Compare to baseline: cargo bench -p uniflight -- --baseline main
+
+#![allow(missing_docs)]
+
+use std::sync::{
+ Arc,
+ atomic::{AtomicU64, Ordering},
+};
+
+use criterion::{Criterion, criterion_group, criterion_main};
+use uniflight::Merger;
+
+static KEY_COUNTER: AtomicU64 = AtomicU64::new(0);
+
+fn unique_key() -> String {
+ format!("key_{}", KEY_COUNTER.fetch_add(1, Ordering::Relaxed))
+}
+
+/// Baseline: single call, no contention.
+/// This measures the fixed overhead of the merger.
+fn bench_single_call(c: &mut Criterion) {
+ let rt = tokio::runtime::Runtime::new().unwrap();
+ let merger = Arc::new(Merger::::new());
+
+ c.bench_function("single_call", |b| {
+ b.to_async(&rt).iter(|| {
+ let merger = Arc::clone(&merger);
+ async move { merger.execute(&unique_key(), || async { "value".to_string() }).await }
+ });
+ });
+}
+
+/// Stress test: 100 concurrent tasks on the same key.
+/// This hammers the synchronization primitives.
+fn bench_high_contention(c: &mut Criterion) {
+ let rt = tokio::runtime::Runtime::new().unwrap();
+ let merger = Arc::new(Merger::::new());
+
+ c.bench_function("high_contention_100", |b| {
+ b.to_async(&rt).iter(|| {
+ let merger = Arc::clone(&merger);
+ async move {
+ let key = unique_key();
+ let tasks: Vec<_> = (0..100)
+ .map(|_| {
+ let merger = Arc::clone(&merger);
+ let key = key.clone();
+ tokio::spawn(async move { merger.execute(&key, || async { "value".to_string() }).await })
+ })
+ .collect();
+
+ for task in tasks {
+ task.await.unwrap();
+ }
+ }
+ });
+ });
+}
+
+/// Distributed load: 10 keys with 10 concurrent tasks each.
+/// This exercises the hash map under concurrent access.
+fn bench_distributed_keys(c: &mut Criterion) {
+ let rt = tokio::runtime::Runtime::new().unwrap();
+ let merger = Arc::new(Merger::::new());
+
+ c.bench_function("distributed_10x10", |b| {
+ b.to_async(&rt).iter(|| {
+ let merger = Arc::clone(&merger);
+ async move {
+ let prefix = KEY_COUNTER.fetch_add(1, Ordering::Relaxed);
+ let tasks: Vec<_> = (0..10)
+ .flat_map(|key_id| {
+ let merger = Arc::clone(&merger);
+ (0..10).map(move |_| {
+ let merger = Arc::clone(&merger);
+ let key = format!("key_{prefix}_{key_id}");
+ tokio::spawn(async move { merger.execute(&key, || async { "value".to_string() }).await })
+ })
+ })
+ .collect();
+
+ for task in tasks {
+ task.await.unwrap();
+ }
+ }
+ });
+ });
+}
+
+criterion_group!(benches, bench_single_call, bench_high_contention, bench_distributed_keys,);
+
+criterion_main!(benches);
diff --git a/crates/uniflight/examples/cache_population.rs b/crates/uniflight/examples/cache_population.rs
new file mode 100644
index 00000000..0e113863
--- /dev/null
+++ b/crates/uniflight/examples/cache_population.rs
@@ -0,0 +1,70 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! Demonstrates using `UniFlight` to prevent thundering herd when populating a cache.
+//!
+//! Multiple concurrent requests for the same cache key will share a single execution,
+//! with the first request (leader) performing the work and subsequent requests (followers)
+//! receiving a copy of the result.
+
+use std::{
+ sync::{
+ Arc,
+ atomic::{AtomicUsize, Ordering},
+ },
+ time::Duration,
+};
+
+use tick::Clock;
+use uniflight::Merger;
+
+#[tokio::main]
+async fn main() {
+ // Create a shared UniFlight instance for cache operations
+ let cache_group = Arc::new(Merger::::new());
+
+ // Track how many times the work closure actually executes
+ let execution_count = Arc::new(AtomicUsize::new(0));
+
+ println!("Starting 5 concurrent requests for user:123...\n");
+
+ // Simulate 5 concurrent requests for the same user data
+ let mut handles = Vec::new();
+ for i in 1..=5 {
+ let group = Arc::clone(&cache_group);
+ let counter = Arc::clone(&execution_count);
+ let handle = tokio::spawn(async move {
+ let clock = Clock::new_tokio();
+ let start = clock.instant();
+
+ let result = group
+ .execute("user:123", || async {
+ let count = counter.fetch_add(1, Ordering::SeqCst) + 1;
+ println!(" [Request {i}] I'm the leader! Fetching from database... (execution #{count})");
+
+ // Simulate expensive database query
+ clock.delay(Duration::from_millis(500)).await;
+
+ "UserData(name: Alice, age: 30)".to_string()
+ })
+ .await;
+
+ let elapsed = start.elapsed();
+ println!(" [Request {i}] Got result in {elapsed:?}: {result}");
+ });
+
+ handles.push(handle);
+
+ // Stagger the requests slightly to see the deduplication in action
+ let clock = Clock::new_tokio();
+ clock.delay(Duration::from_millis(10)).await;
+ }
+
+ // Wait for all requests to complete
+ for handle in handles {
+ handle.await.expect("Task panicked");
+ }
+
+ let total_executions = execution_count.load(Ordering::SeqCst);
+ println!("\nAll requests completed! Database query executed {total_executions} time(s) for 5 requests.");
+}
diff --git a/crates/uniflight/favicon.ico b/crates/uniflight/favicon.ico
new file mode 100644
index 00000000..f1a3f34c
--- /dev/null
+++ b/crates/uniflight/favicon.ico
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:ed616c6fc5b1c95300147e226f28d1b88f193babdf3bb1669422a93f55339304
+size 15406
diff --git a/crates/uniflight/logo.png b/crates/uniflight/logo.png
new file mode 100644
index 00000000..1562ae9f
--- /dev/null
+++ b/crates/uniflight/logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6bf624b54edbaeb8bf0d961e895a86e09b18502fe6c761d00748317883dd09b8
+size 62560
diff --git a/crates/uniflight/src/lib.rs b/crates/uniflight/src/lib.rs
new file mode 100644
index 00000000..987b4c2e
--- /dev/null
+++ b/crates/uniflight/src/lib.rs
@@ -0,0 +1,483 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+//! 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
+//!
+//! ```
+//! use uniflight::Merger;
+//!
+//! # async fn example() {
+//! let group: Merger = 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;
+//! # }
+//! ```
+//!
+//! # Flexible Key Types
+//!
+//! The [`Merger::execute`] method accepts keys using [`Borrow`] semantics, allowing you to pass
+//! borrowed forms of the key type. For example, with `Merger`, you can pass `&str`
+//! directly without allocating:
+//!
+//! ```
+//! # use uniflight::Merger;
+//! # async fn example() {
+//! let merger: Merger = Merger::new();
+//!
+//! // Pass &str directly - no need to call .to_string()
+//! merger.execute("my-key", || async { 42 }).await;
+//! # }
+//! ```
+//!
+//! # Thread-Aware Scoping
+//!
+//! `Merger` supports thread-aware scoping via a [`Strategy`]
+//! type parameter. This controls how the internal state is partitioned across threads/NUMA nodes:
+//!
+//! - [`PerProcess`] (default): Single global state, maximum deduplication
+//! - [`PerNuma`]: Separate state per NUMA node, NUMA-local memory access
+//! - [`PerCore`]: Separate state per core, no deduplication (useful for already-partitioned work)
+//!
+//! ```
+//! use uniflight::Merger;
+//! use thread_aware::PerNuma;
+//!
+//! # async fn example() {
+//! // NUMA-aware merger - each NUMA node gets its own deduplication scope
+//! let merger: Merger = Merger::new_per_numa();
+//! # }
+//! ```
+//!
+//! # 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
+//!
+//! # Memory Management
+//!
+//! Completed entries are automatically removed from the internal map when the last caller
+//! finishes. This ensures no stale entries accumulate over time.
+//!
+//! # 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
+//!
+//! 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.
+
+#![doc(html_logo_url = "https://media.githubusercontent.com/media/microsoft/oxidizer/refs/heads/main/crates/uniflight/logo.png")]
+#![doc(html_favicon_url = "https://media.githubusercontent.com/media/microsoft/oxidizer/refs/heads/main/crates/uniflight/favicon.ico")]
+
+use std::{
+ borrow::Borrow,
+ fmt::Debug,
+ hash::Hash,
+ sync::{Arc, Weak},
+};
+
+use async_once_cell::OnceCell;
+use dashmap::{
+ DashMap,
+ Entry::{Occupied, Vacant},
+};
+use thread_aware::{
+ Arc as TaArc, PerCore, PerNuma, PerProcess, ThreadAware,
+ affinity::{MemoryAffinity, PinnedAffinity},
+ storage::Strategy,
+};
+
+/// Suppresses duplicate async operations identified by a key.
+///
+/// The `S` type parameter controls the thread-aware scoping strategy:
+/// - [`PerProcess`]: Single global scope (default, maximum deduplication)
+/// - [`PerNuma`]: Per-NUMA-node scope (NUMA-local memory access)
+/// - [`PerCore`]: Per-core scope (no deduplication)
+pub struct Merger {
+ inner: TaArc>>, S>,
+}
+
+impl Debug for Merger {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ f.debug_struct("Merger").field("inner", &format_args!("DashMap<...>")).finish()
+ }
+}
+
+impl Clone for Merger {
+ fn clone(&self) -> Self {
+ Self { inner: self.inner.clone() }
+ }
+}
+
+impl Default for Merger
+where
+ K: Hash + Eq + Send + Sync + 'static,
+ T: Clone + Send + Sync + 'static,
+ S: Strategy + Send + Sync,
+{
+ fn default() -> Self {
+ Self {
+ inner: TaArc::new(DashMap::new),
+ }
+ }
+}
+
+impl Merger
+where
+ K: Hash + Eq + Send + Sync + 'static,
+ T: Clone + Send + Sync + 'static,
+ S: Strategy + Send + Sync,
+{
+ /// Creates a new `Merger` instance.
+ ///
+ /// The scoping strategy is determined by the type parameter `S`:
+ /// - [`PerProcess`] (default): Process-wide scope, maximum deduplication
+ /// - [`PerNuma`]: Per-NUMA-node scope, NUMA-local memory access
+ /// - [`PerCore`]: Per-core scope, no cross-core deduplication
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// use uniflight::Merger;
+ /// use thread_aware::{PerNuma, PerCore};
+ ///
+ /// // Default (PerProcess) - type can be inferred
+ /// let global: Merger = Merger::new();
+ ///
+ /// // NUMA-local scope
+ /// let numa: Merger = Merger::new();
+ ///
+ /// // Per-core scope
+ /// let core: Merger = Merger::new();
+ /// ```
+ #[inline]
+ #[must_use]
+ pub fn new() -> Self {
+ Self::default()
+ }
+}
+
+impl Merger
+where
+ K: Hash + Eq + Send + Sync + 'static,
+ T: Clone + Send + Sync + 'static,
+{
+ /// Creates a new `Merger` with process-wide scoping (default).
+ ///
+ /// All threads share a single deduplication scope, providing maximum
+ /// work deduplication across the entire process.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use uniflight::Merger;
+ ///
+ /// let merger = Merger::::new_per_process();
+ /// ```
+ #[inline]
+ #[must_use]
+ #[cfg_attr(test, mutants::skip)] // Equivalent mutant: delegates to Default
+ pub fn new_per_process() -> Self {
+ Self::default()
+ }
+}
+
+impl Merger
+where
+ K: Hash + Eq + Send + Sync + 'static,
+ T: Clone + Send + Sync + 'static,
+{
+ /// Creates a new `Merger` with per-NUMA-node scoping.
+ ///
+ /// Each NUMA node gets its own deduplication scope, ensuring memory
+ /// locality for cached results while still deduplicating within each node.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use uniflight::Merger;
+ ///
+ /// let merger = Merger::::new_per_numa();
+ /// ```
+ #[inline]
+ #[must_use]
+ #[cfg_attr(test, mutants::skip)] // Equivalent mutant: delegates to Default
+ pub fn new_per_numa() -> Self {
+ Self::default()
+ }
+}
+
+impl Merger
+where
+ K: Hash + Eq + Send + Sync + 'static,
+ T: Clone + Send + Sync + 'static,
+{
+ /// Creates a new `Merger` with per-core scoping.
+ ///
+ /// Each core gets its own deduplication scope. This is useful when work
+ /// is already partitioned by core and cross-core deduplication is not needed.
+ ///
+ /// # Example
+ ///
+ /// ```
+ /// use uniflight::Merger;
+ ///
+ /// let merger = Merger::::new_per_core();
+ /// ```
+ #[inline]
+ #[must_use]
+ #[cfg_attr(test, mutants::skip)] // Equivalent mutant: delegates to Default
+ pub fn new_per_core() -> Self {
+ Self::default()
+ }
+}
+
+impl Merger
+where
+ K: Hash + Eq,
+{
+ /// Returns the number of in-flight operations.
+ #[cfg(test)]
+ fn len(&self) -> usize {
+ self.inner.len()
+ }
+
+ /// Returns `true` if there are no in-flight operations.
+ #[cfg(test)]
+ fn is_empty(&self) -> bool {
+ self.inner.is_empty()
+ }
+}
+
+impl ThreadAware for Merger
+where
+ S: Strategy,
+{
+ fn relocated(self, source: MemoryAffinity, destination: PinnedAffinity) -> Self {
+ Self {
+ inner: self.inner.relocated(source, destination),
+ }
+ }
+}
+
+impl Merger
+where
+ K: Hash + Eq + Send + Sync,
+ T: Clone + Send + Sync,
+ S: Strategy + Send + Sync,
+{
+ /// Execute and return the value for a given function, making sure that only one
+ /// operation is in-flight at a given moment. If a duplicate call comes in,
+ /// that caller will wait until the leader completes and return the same value.
+ ///
+ /// The key can be passed as any borrowed form of `K`. For example, if `K` is `String`,
+ /// you can pass `&str` directly:
+ ///
+ /// ```
+ /// # use uniflight::Merger;
+ /// # async fn example() {
+ /// let merger: Merger = Merger::new();
+ /// let result = merger.execute("my-key", || async { 42 }).await;
+ /// # }
+ /// ```
+ pub fn execute(&self, key: &Q, func: F) -> impl Future