OnceMap and singleflight leak an entry when a duplicate key's destructor panics
维护者通常 1 天内回复
@Kaileshwar16 已经在做这个了。
开始于 2026年10月31日。
评估
调研方向
Start with asyncband/src/once/once_map/mod.rs around get_or_insert and the pending-entry cleanup guard, then inspect asyncband/src/singleflight/mod.rs around get_or_insert and WorkCleanupGuard::new. Reproduce the panic cases described in the issue and trace what happens on pending and cached paths. Done means both APIs clean up correctly after a duplicate key's destructor panics, including cancellation during the drop.
由索引模型根据 Issue 内容生成。
描述
Summary
While working on #387 I found another way OnceMap can keep an empty entry. When compute or try_compute joins a computation that is already running for the key, the caller's own key is no longer needed and is dropped. If that destructor panics and the panic is caught, for example by tokio::spawn, the caller's reference to the entry leaks. Once the original computation is cancelled or fails, the empty entry stays in the map for good. singleflight::Group::work and try_work have the same problem.
Reproducer
On main (8febdc7, Rust 1.96.0) this prints OnceMap { len: 1, pending: 1 }, and the same program with singleflight::Group::work prints Group { in_flight: 1 }. Both are empty when the destructor doesn't panic.
use std::future::pending;
use std::hash::{Hash, Hasher};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::pin::pin;
use std::task::{Context, Waker};
use asyncband::once::OnceMap;
struct Key(&'static str, bool);
impl PartialEq for Key {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for Key {}
impl Hash for Key {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl Drop for Key {
fn drop(&mut self) {
if self.1 {
panic!("key destructor");
}
}
}
fn main() {
let mut cx = Context::from_waker(Waker::noop());
let map = OnceMap::<Key, i32>::new();
let mut original = Box::pin(map.compute(Key("k", false), async || pending().await));
assert!(original.as_mut().poll(&mut cx).is_pending());
let joined = catch_unwind(AssertUnwindSafe(|| {
let _ = pin!(map.compute(Key("k", true), async || pending().await)).poll(&mut cx);
}));
assert!(joined.is_err());
drop(original);
println!("{map:?}");
}
The leaked reference also keeps later values alive: if the key is computed again and then discarded, the value is never dropped. On a cached hit, the value clone that compute was about to return leaks instead.
Cause
The duplicate key is a parameter of get_or_insert (OnceMap, singleflight), so it is dropped when the function returns. If that destructor panics, Rust doesn't drop the value being returned (rust-lang/rust#47949, open since 2018), so the entry's Arc, or the cloned value on a cached hit, leaks. Even if it were dropped, no cleanup would run, because the guard is only created after get_or_insert returns. On OnceMap, making the drop explicit without moving the guard removes the permanent leak, but a cancellation that lands while the destructor runs still leaves an empty entry.
Possible fix
Drop the duplicate key explicitly, after creating the cleanup guard for a pending entry or cloning the value for a ready one. I tried this on OnceMap by having get_or_insert return either the value or the guard. Nothing leaks in the cases above, and the cached hit path still doesn't build a guard. singleflight would need the same change around WorkCleanupGuard::new.
Panicking destructors are unusual, so documenting the leak is also an option. I'd prefer to fix both, since the change is small. @tisonkun What do you think?
- 主要语言
- Rust
- 星标
- 288
- 派生
- 50
- 平均合并
- 2 天 1 小时
- 30 天内合并 PR
- 35
环境准备
这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
apache/asyncband 的其他 Issue
-
enhancement good first issue help wanted
难度 5/5 一周以上 新手友好度 35/100
apache/asyncband#324 · 2 条评论 ·
维护者通常 1 天内回复
-
enhancement help wanted
难度 5/5 一周以上 新手友好度 35/100
apache/asyncband#298 · 8 条评论 ·
维护者通常 1 天内回复
-
难度 5/5 一周以上 新手友好度 30/100
apache/asyncband#272 · 2 条评论 ·
维护者通常 1 天内回复
-
Explore ManualResetEvent waiter storage and state fast paths可能重新可做 关联的 PR 已关闭且未合并。 未关闭enhancement good first issue help wanted
难度 5/5 一周以上 新手友好度 38/100
apache/asyncband#252 · 1 条评论 ·
维护者通常 1 天内回复
-
enhancement
难度 5/5 一周以上 新手友好度 20/100
apache/asyncband#224 · 1 条评论 ·
维护者通常 1 天内回复
相似的 Issue
-
难度 2/5 1-3 小时 新手友好度 78/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 68/100
longbridge/nsh#804 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 62/100
维护者通常 1 天内回复
-
devex good first issue
难度 2/5 1-3 小时 新手友好度 62/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 62/100
calcit-lang/calcit#2538 ·
维护者通常 1 天内回复