1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
//! Shared per-key single-flight gate (#1201, generalized #1228).
//!
//! Session resolution is lookup-then-create, and the two steps are not
//! atomic. Two messages landing in a brand-new chat/topic ~90 ms apart both
//! miss the lookup and both create a session, so two full turns run in
//! parallel against one key and the orphaned session's context is lost.
//!
//! The "session busy, queue into the in-flight turn" guard cannot help with
//! this, because neither turn is streaming yet when the other resolves — the
//! race is entirely inside the window before any session exists.
//!
//! Both creations happen on one thread, so this is not a cross-thread data
//! race on a shared map. It is two independent async tasks interleaving at an
//! await point between the lookup and the insert, which a `Mutex` around the
//! map alone would not close: the lock has to span BOTH steps. That is what
//! this module provides — a per-key async gate whose guard lives for the whole
//! resolve-or-create.
//!
//! Originally written for the Telegram forum-topic case keyed on `(chat_id,
//! topic_id)` (#1201). Generalizing the key to `String` lets every channel
//! resolver reuse the same gate instead of re-implementing it.
use HashMap;
use ;
use ;
/// Hold the resolution gate for `key` until the guard drops.
///
/// The second caller waits for the first to finish resolving, and then finds
/// the session the first created rather than creating its own.
///
/// `key` must identify the entity whose resolution must be single-flight
/// (chat id, chat+topic pair, channel id — whatever the caller resolves by).
/// Distinct keys never block each other.
///
/// A poisoned registry is recovered from rather than propagated: refusing to
/// resolve a session is a worse outcome than the duplicate this prevents.
pub async
/// How many keys are being tracked. Exposed for tests.