Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

OnceMap and singleflight leak an entry when a duplicate key's destructor panics

オープン
#388 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
1〜2日
初心者へのやさしさ
55/100
issue の種類
バグ
明瞭さ
明確に書かれている
活発さ
活発
技術スタック
rust
領域
backend

調査の方向性

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
スター
285
フォーク
49
平均マージ
1日 1時間
マージ済み PR(30日)
33

環境構築

このプロジェクトには開発コンテナ、Dockerfile、コントリビューションガイドがありません。まず README を読み、一般的な手順ははじめてのコントリビューションガイドを参照してください。

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

apache/asyncband のほかの issue

apache/asyncband の issue をすべて見る

似ている issue

Rust の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。