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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
//! Blocking-pool admission control for windowed streaming scans (issue #1594,
//! Epic F finding F4).
//!
//! # Why this exists
//!
//! `run_scan_stream_windowed` spawns a long-lived `spawn_blocking` PARSE task per
//! scan (issue #1143), and — for a synchronously-faulting backend (mmap page
//! fault / `O_DIRECT` `pread`) — a SECOND long-lived `spawn_blocking` FEED task
//! per scan (issue #1593, F3). So a faulting-backend scan pins TWO blocking-pool
//! threads for its full duration; `K` concurrent cold scans pin `~2K` threads.
//! tokio's blocking pool defaults to 512 threads and is SHARED with tokio-fs
//! internals, so at high scan concurrency latency-critical point-read file ops
//! queue behind these long-lived throughput tasks — the priority inversion the
//! July 2026 read-path audit named (§Epic F / F4).
//!
//! # The mechanism
//!
//! A process-wide [`tokio::sync::Semaphore`] caps the number of scan OPERATIONS
//! admitted to the blocking pool concurrently. Admission is per top-level scan
//! OPERATION — NOT per blocking thread and NOT per fan-out sub-scan (issue #1594
//! deadlock fix):
//!
//! - A direct single-generation scan
//! ([`run_scan_stream`](super::SSTableReader::run_scan_stream) invoked with
//! [`ScanAdmission::Acquire`]) acquires exactly ONE permit ([`admit`]) at the
//! TOP of the scan — BEFORE any `spawn_blocking` — and holds it via the RAII
//! [`ScanAdmissionPermit`] guard for the whole scan.
//! - A cross-generation fan-out merge (`SSTableManager::scan_stream`) is ONE
//! operation that legitimately opens N per-generation sub-scans and primes a
//! head from EVERY sub-scan before draining any. It acquires exactly ONE permit
//! for the whole merge and opens each sub-scan with [`ScanAdmission::Exempt`], so
//! the sub-scans do NOT independently admit. A single query therefore needs
//! exactly ONE permit, never N.
//!
//! The permit (and therefore the admission slot) is returned on EVERY exit path —
//! success, error, or cancellation/drop — because the guard's `Drop` returns the
//! owned permit. A scan can never leak a slot.
//!
//! One permit per scan OPERATION (not per blocking thread, not per sub-scan): the
//! bound limits concurrent INDEPENDENT scan operations (the K-concurrent-cold-scans
//! case the July 2026 audit named). For the common single-generation table each
//! admitted operation owns its (up to two, faulting-backend) blocking threads, so
//! `cap` operations bound the footprint to `~2 × cap` threads — a small fraction of
//! tokio's 512-thread default, leaving ample headroom for latency-critical fs/point
//! ops. A cross-generation operation's fan-out inherently needs its N sub-scans
//! live AT ONCE (it primes every head before draining), so that intra-operation
//! fan-out is deliberately NOT throttled: throttling it would deadlock (see below).
//! The admission bound is therefore on operation concurrency, not on the total
//! blocking threads a single multi-generation operation may transiently use.
//!
//! # Scope
//!
//! Admission covers BOTH the LAZY/WINDOWED scan path AND the write-support
//! multi-generation EAGER-materialize path (issue #2063). The eager path is THREE
//! helpers in `storage/sstable/generation_merge.rs`, each draining a `KWayMerger`
//! inside a single `spawn_blocking`, each acquiring exactly ONE operation-level
//! permit at the top of the function through this same process-wide semaphore:
//!
//! - `merge_generations_for_read` — the materializing plain read (`scan` / range /
//! partition-targeted point read).
//! - `seek_merge_generations_for_read` — the partition-SEEKING point-read merge
//! (`scan_partition_clustering`, multi-candidate `WHERE pk = ?`). Its only call
//! site is that top-level manager operation, never nested under another admitted
//! operation.
//! - `merge_generations_for_read_with_metadata` — the `WRITETIME`/`TTL` projection
//! sibling.
//!
//! That is the precise fit for the eager shape (one `spawn_blocking`, no async
//! fan-out, no sub-scans) and cannot reintroduce the #1594 hold-and-wait deadlock:
//! each is a single top-level once-only `admit()` with no nested acquisition (the
//! KWayMerger's producer OS threads never call [`admit`]).
//!
//! CANCELLATION (issue #2063): ALL THREE helpers hold the permit until the detached
//! `spawn_blocking` merge TERMINATES, so no phase both runs detached blocking work AND
//! has already released the permit — repeated cancels can never exceed the bound. The
//! two PURE-blocking helpers (`merge_generations_for_read`,
//! `seek_merge_generations_for_read`) MOVE the permit INTO the `spawn_blocking` closure
//! directly. The METADATA helper holds the permit as an OUTER future guard across its
//! async per-reader `scan_with_cell_metadata` loop (cancellation there is clean — no
//! detached blocking work exists yet, so early release is harmless), THEN MOVES it into
//! the `spawn_blocking` merge closure for the detached phase. The three helpers are
//! therefore UNIFORMLY cancellation-safe.
//!
//! KNOWN LIMITATION (documented, not solved here): the shared semaphore bounds
//! eager *operation* concurrency, NOT the eager path's per-operation resource
//! footprint. Unlike the windowed path (tokio blocking-pool threads), each eager
//! operation drives `M` `std::thread` producer threads (M = generation count;
//! KWayMerger, #2316), so `cap` admitted eager operations can still spawn up to
//! `cap × M` producer OS threads. The bound limits how many eager operations run
//! at once, consistent with #1594's "operation concurrency, not total blocking
//! threads" semantic; sizing the eager path's thread footprint separately is a
//! future issue (see the change's Non-goals).
//!
//! # Deadlock-freedom
//!
//! Admission is per top-level scan OPERATION; a fan-out merge's sub-scans are
//! EXEMPT and never independently admit. No code path blocks on [`admit`] while
//! already holding a permit, and no permit-holder's progress depends on another
//! permit being granted: an operation acquires AT MOST ONE permit, once, at its
//! top, holding no other permit/lock while awaiting, and its internal fan-out
//! sub-scans run permit-free. Independent operations that queue at [`admit`] hold
//! nothing, so they cannot block a permit-holder. Hence there is no hold-and-wait
//! cycle — the mechanism stays deadlock-free even when a single query fans out to
//! `N > cap` generations.
//!
//! This is the exact bug an earlier per-SUB-SCAN design introduced: with each
//! sub-scan admitting independently, a fan-out to `N > cap` generations let `cap`
//! sub-scans win permits and park in consumer backpressure while the remaining
//! sub-scans blocked forever at `admit`; the merge — stuck priming a head from
//! EVERY sub-scan — never drained the permit-holders, so no permit ever freed
//! (permanent hang). Admitting at operation granularity removes the hold-and-wait.
//!
//! Queue-full behavior is WAIT, not error: when `cap` operations are admitted the
//! `cap + 1`-th operation simply blocks at `admit().await` until a permit frees,
//! then proceeds (natural backpressure).
use ;
use ;
/// Whether a windowed scan must acquire its own admission permit, or is already
/// covered by a caller's permit.
///
/// Admission is per top-level scan OPERATION (issue #1594). A cross-generation
/// fan-out merge is ONE operation that opens N sub-scans and primes a head from
/// every one before draining any; if each sub-scan admitted independently, a
/// fan-out to `N > cap` generations would deadlock (`cap` sub-scans hold permits
/// and park in backpressure while the rest block forever at [`admit`], and the
/// priming merge never drains the holders). So the fan-out acquires ONE permit for
/// the whole operation and opens each sub-scan [`Exempt`](ScanAdmission::Exempt).
pub
/// Default cap on concurrently-admitted windowed scans.
///
/// Derived from `available_parallelism`: the windowed parse half is CPU-bound, so
/// admitting more concurrent scan OPERATIONS than cores yields no throughput, only
/// pressure on the shared blocking pool. For the common single-generation table a
/// faulting-backend operation holds TWO blocking threads per admitted permit (issue
/// #1593, F3's doubled footprint), so a cap of `ncpu` bounds that footprint to
/// `~2 × ncpu` — a small fraction of tokio's 512-thread default pool — leaving
/// ample headroom for latency-critical fs/point-read operations regardless of
/// operation concurrency `K`. (A cross-generation operation's fan-out inherently
/// uses more, one pair per sub-scan, because it needs all N sub-scans live at once;
/// that intra-operation fan-out is deliberately not throttled — see the module
/// docs — so the cap bounds operation concurrency, not one operation's fan-out.)
/// Never below 1 (a zero-permit semaphore would deadlock every scan).
/// The process-wide admission semaphore used in production. Lazily initialized
/// once from [`default_limit`]; no per-scan lock on the hot path.
/// The admission semaphore this process should use right now. In default/release
/// builds this is always the production semaphore; under the non-default
/// `scan-offload-probe` feature a test may install a low-cap override.
/// Acquire one admission permit for a scan OPERATION against the process-wide
/// semaphore, waiting if the admission limit is currently reached. See
/// [`admit_with`] for the fail-open / no-panic contract. Callers acquire at most
/// ONE permit per top-level operation (a fan-out merge's sub-scans are
/// [`ScanAdmission::Exempt`] and do not call this) — see the module docs.
pub async
/// Acquire one admission permit against an EXPLICIT semaphore (production
/// [`admit`] passes the process-wide one; unit tests pass an isolated local one).
///
/// Waits when no permit is available (natural backpressure — never errors from
/// queue-full). `acquire_owned` errors only if the semaphore is CLOSED, which
/// never happens for our never-closed semaphore; to honor the no-`unwrap`/`expect`
/// rule AND guarantee admission control can never make a scan un-runnable, that
/// impossible case is FAIL-OPEN: proceed without a permit rather than panic.
async
/// RAII admission slot for one windowed scan.
///
/// Holds the owned semaphore permit (when one was granted) for the scan's whole
/// duration. Dropping this guard — on success, error, or cancellation/drop —
/// returns the permit to the semaphore, releasing the admission slot. `Drop`
/// performs only atomic bookkeeping + the owned-permit drop and never panics
/// (no-panic-in-`Drop`).
pub
/// Test-only admission instrumentation (issue #1594 wiring guard). Compiled ONLY
/// under the non-default `scan-offload-probe` feature: in a normal/default/release
/// build this module, its statics, and its call-sites do not exist, so admission
/// control adds zero test surface and no public API. Exposed as `pub` (via the
/// parent's feature-gated `pub mod scan_admission`) so the integration guard can
/// install a low cap and read the in-flight counters.