Skip to content

fix: clean up abandoned keyed once cells - #142

Merged
tisonkun merged 9 commits into
mainfrom
codex/cleanup-empty-once-cells
Aug 9, 2026
Merged

fix: clean up abandoned keyed once cells#142
tisonkun merged 9 commits into
mainfrom
codex/cleanup-empty-once-cells

Conversation

@tisonkun

@tisonkun tisonkun commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • remove uninitialized OnceMap and singleflight::Group entries after the last active call fails, panics, or is cancelled
  • preserve a shared entry while another caller can still retry the same in-flight computation
  • use hashbrown::HashTable to make exact abandoned-entry cleanup expected O(1), without an active-call counter or an extra atomic RMW on every call
  • retain borrowed-key lookup and avoid adding Clone bounds to keys and custom hashers
  • keep hash-table handles and OnceCell access behind the internal OnceTable and OnceTableEntry APIs
  • cover cached, successful, failed, cancelled, panicking, and retrying keyed-once operations with tests and Divan benchmarks
  • align the crate-visible OnceCell::initialized_mut query with initialized

Design

This revisits #133 with one ownership invariant:

  • the table owns one strong Arc<OnceTableEntry<K, V>>
  • each active call owns one strong reference through its cleanup guard

The entry stores its precomputed hash, key, and OnceCell. Moving the key into the shared entry lets a call retain entry identity without cloning K. When an unsuccessful call exits, its guard locks the table while retaining its Arc. If the entry is still uninitialized and the strong count is two, the table and the current call are its only owners. The table then removes that exact allocation using its cached hash and Arc::ptr_eq.

Allocation identity is required because forget, discard, or remove can detach an old entry and a later call can insert a replacement for the same key. A stale guard must not remove the replacement.

HashTable is used instead of HashMap::retain or HashMap::raw_entry_mut:

  • retain scans every entry and made failed cleanup O(n)
  • a raw HashMap lookup can match only on the key, while exact cleanup must match the entry allocation after the guard has transferred ownership of K
  • HashTable can hash into the relevant bucket and compare the complete entry, giving expected O(1) exact removal

The hash-table details stay in one private OnceTable. Its lookup and insertion methods return logical entries rather than exposing hashbrown entry handles. OnceTableEntry owns value inspection and initialization, so OnceMap and Group do not reach through it to the underlying OnceCell. Cleanup guards keep the lifecycle rule—the last caller removes an uninitialized entry—at the call site. Group removes a completed entry inside the initializer before OnceCell publishes the result. The cached OnceMap path clones the value directly while holding the table lock, so it does not add an Arc RMW.

hashbrown 0.17.1 has the same Rust 1.85 MSRV as mea. Default features are disabled and only inline-more is enabled, so this adds no transitive normal dependency.

Benchmarks

These are Divan medians from an arm64 Linux OrbStack VM using Rust 1.85.1, comparing the previous O(n) implementation at 5899bbf with this update. The successful paths are unchanged in behavior; the failure rows include cleanup.

Case O(n) cleanup HashTable cleanup Change
OnceMap cached 21.06 ns 20.77 ns -1%
OnceMap vacant success 56.7 ns 57.39 ns +1%
OnceMap error, 0 entries 65.51 ns 65.2 ns unchanged
OnceMap error, 64 entries 205.4 ns 70.08 ns -66%
OnceMap error, 1024 entries 2.187 µs 84.4 ns -96%
Group success 86.32 ns 69.43 ns -20%
Group error 79.82 ns 66.83 ns -16%

The cached and vacant OnceMap paths remain within measurement noise. Failed cleanup no longer scales linearly with the number of cached entries. macOS measurements show the same shape, including a reduction from about 2.10 µs to 208 ns for the 1024-entry failure case. A focused before/after run for the final structural simplification measured the extended vacant-success case at the same 114.2 ns median on both revisions.

Verification

  • cargo x lint
  • cargo test --workspace --all-features
  • cargo +1.85.0 check --workspace --all-targets
  • cargo +1.85.0 test --workspace --no-default-features
  • cargo x semver --release-version 0.6.6
  • cargo bench --bench primitives -- --test

@tisonkun
tisonkun enabled auto-merge (squash) August 9, 2026 18:11
@tisonkun
tisonkun merged commit c5c583f into main Aug 9, 2026
10 checks passed
@tisonkun
tisonkun deleted the codex/cleanup-empty-once-cells branch August 9, 2026 18:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant