T17 concurrent admission diagnosis
Investigated on 2026-10-18 after committing the legacy integration repairs as
b4be414f. All Catalog B feature tickets are implemented; this follows up the
remaining 32-caller acceptance blocker. No production code, memory limits,
protector policy, protobuf or benchmark acceptance rules changed.
Finding
The 32-caller failure is a legitimate shared-pool rejection under the current native receive reservation policy. It is independent of top-k state and can occur before any input rows are read. Thirty-two clients are not a promise that all 32 queries can simultaneously retain their maximum allowed transport buffers.
The fixed Docker container has four CPUs, 8 GiB memory and no additional swap. The benchmark starts two data nodes and one liaison in the same process. Each node creates its own protector and query pool; these are not one combined pool for the test process. The following calculation concerns the liaison pool.
| Budget or reservation | Value |
|---|---|
| Default protector limit: 75% of 8 GiB | 6 GiB |
| Query pool: protector limit / 4 | 1,536 MiB |
| Per-query admission ceiling: pool / 8 | 192 MiB |
| Separate vectorized tracker ceiling | 256 MiB |
| Initial query admission charge | 1 MiB |
| Fixed workspace for two streams | 640 KiB |
| Per-stream maximum message allowance | 8,317,610 bytes |
| Two-stream receive reservation | 50,561,020 bytes |
| Admission plus receive reservation | 49.218746 MiB |
QueryBudget.AdmitMeasureContext charges the initial 1 MiB.
NativeReceiveScope.ReserveFanout uses the smaller remaining tracker/admission
allowance: 191 MiB here. Each stream needs a 64-KiB static receive window and
256-KiB parser workspace. One quarter of the remaining allowance covers three
payload copies, with the message cap divided between the two streams:
available = 192 MiB - 1 MiB
fixed = 2 * (64 KiB + 256 KiB)
message_cap = floor((available - fixed) / 4 / 3 / 2)
receive_reservation = fixed + 3 * message_cap * 2
32 * (1 MiB + receive_reservation) = 1,574.999878 MiB > 1,536 MiB
Additional allocations before fanout cannot improve this bound: they increase parent commitment while only reducing the receive allowance by approximately a quarter of that increase. Local tracker releases also do not refund the parent’s cumulative lease charge; parent reservations remain owned until transport/query completion. This is intentional ownership accounting, not evidence of a leak.
The reservation covers the permitted maximum message, not the actual 1,024-row fixture size. Dropping the reservation while retaining the same receive cap would undercount possible transport memory. A different reservation/transport strategy would require a separate optimization with large-response and concurrency proof. This investigation does not introduce one or relax protection to pass a gate.
Reproduction and controls
The existing TestQLBT17Admission was rerun at b4be414f with querymetrics,
DQB_T17_ADMISSION_ROWS=1024, RUN_DISTRIBUTED_QUERY_BENCH=1,
DQB_IN_CONTAINER=1 and DQB_ENGINE=measure inside the fixed Docker container.
The test fails in 8.99 seconds at composed_workers_32 with:
ResourceExhausted: ... vec distributed plan: broadcast: query memory pool exhausted
Cleanup succeeds. Evidence:
.omx/t17-next-admission-baseline.log and
.omx/t17-next-admission-baseline/t17-admission-1024.json.
The existing TestQLBT17ConcurrencyControl, enabled by
DQB_T17_CONCURRENCY_CONTROL=1 with the same limits, runs the forced-full-sort
reference. It records 32 peak client calls, 95/96 completed queries, the same
broadcast rejection and successful cleanup. The diagnostic test itself exits
successfully because it records this expected control result; the report’s
correctness remains blocked, and throughput_evidence is false. Evidence:
.omx/t17-next-fullsort-control.log and
.omx/t17-next-fullsort-control/fullsort32-control/t17-admission-1024.json.
An ignored minimal executable probe exercises the real QueryBudget,
MemoryTracker, NativeReceiveScope and transport ownership with a static
6-GiB protector. It holds transport scopes open to remove scheduling ambiguity:
- 31 receive fanouts are admitted; fanout 32 returns
query memory pool exhausted. - The failed reservation rolls back its local tracker charge.
- Local scope/lease closes retain transport-owned pool reservations correctly.
- Closing all transports returns the shared pool to zero.
- A subsequent query succeeds; its close returns the pool to zero again.
The probe and exact byte results are retained as
.omx/t17-admission-contract-probe/main.go and result.json. This isolates the
cause without query parsing, data ingestion or sorting. The normal 32-client
benchmark can occasionally succeed through staggered completion; such a run
would not disprove this simultaneous-residency bound.
Acceptance consequence
The existing strict benchmark remains partial_known_32_blocked; this diagnosis
does not convert a rejected query into a successful timing sample or retroactively
pass T17. It identifies why mandatory success at 32 clients is not a stable
capacity-independent gate under the fixed reservation policy.
The user approved the revised acceptance criteria
on 2026-10-18: compare full sort/top-k on fixed, fully admitted timing cohorts;
check 32-caller overload separately for explicit ResourceExhausted, complete
cleanup and subsequent recovery. The subsequent harness implementation adds
separate overload/recovery verification and same-cluster RPC recovery. Historical
reports retain their original verdicts; the minimal probe above remains evidence
of admission recovery rather than an RPC recovery test. A requirement for 32 simultaneously resident queries would
instead require a separately justified transport/reservation optimization.
The existing 1/8-caller measurements remain evidence for their recorded revisions.
Large-input RPC/full compatibility performance evidence and historical T02
acceptance remain separate outstanding work. Spilling and retained-result
pagination sessions remain deferred.