Skip to content

fix(once_map): clone only initialized entries in get - #387

Merged
orthur2 merged 2 commits into
apache:mainfrom
orthur2:fix/once-map-pending-get
Oct 26, 2026
Merged

orthur2 merged 2 commits into
apache:mainfrom
orthur2:fix/once-map-pending-get

Conversation

@orthur2

@orthur2 orthur2 commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Summary

OnceMap::get clones the entry's Arc under the table lock and reads the cell after unlocking. If the key is still being computed and that computation fails, panics, or is cancelled inside this window, its cleanup sees three owners (the table, itself, and the reader) and leaves the entry in place. The reader then returns None and drops its clone without cleaning up, so the empty entry stays in the map until the same key is computed or discarded again. With distinct keys these entries pile up: locally, four threads calling get on the key under computation while 20,000 try_compute calls failed left about 9,000 to 9,800 empty entries.

get has no use for a pending entry, since it returns None for it anyway, so this change only clones entries that are already initialized. Initialized entries are never reclaimed, so a reader holding one cannot interfere with cleanup, and pending lookups also skip the Arc clone and drop.

A new unit test cancels a computation while holding the result of the lookup get performs, which is the window where get used to keep the entry alive, and checks that no entry is left. It fails on the previous implementation with one entry left. The faulty behavior is in v0.7.4, so the fix has a changelog entry.

Validation: cargo x test (612 passed), cargo x check (26 feature configurations), cargo x lint, and the once_map unit tests under Miri.

A get that cloned a pending entry kept the cleanup of a failed, panicked,
or cancelled computation from removing it, so the empty entry stayed in
the map. get returns None for a pending entry anyway, so it now clones
only initialized entries, which are never reclaimed.
Comment thread asyncband/src/once/once_map/mod.rs Outdated
Comment on lines +135 to +136
#[cfg(test)]
tests::after_get_lookup();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have this #[cfg(test)] sentence?

Can we get rid of it in some way?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It let the test pause get right after its lookup so the computation could be cancelled in that window.

I thought about it again, and that wasn’t a good approach. The test can hold find_entry’s result across the cancellation on one thread instead, so I removed the hook. Since that is less obvious than a paused get, I added a comment saying it stands in for a concurrent get.

The regression test now holds find_entry's result across the cancellation
on one thread, which is the window get used to keep the entry alive in,
so get needs no test-only code.

@tisonkun tisonkun left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix!

@orthur2
orthur2 merged commit 8febdc7 into apache:main Oct 26, 2026
9 checks passed
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.

2 participants