Context (measured, #359)
The arq_e2e bench proved both directions of the ACK contract on the 8812EU: with the default async ring, receiver-internal congestion (150 ms consumer freezes against a ~2.3 k fps flood) makes the chip decline the ACK — 14,214/14,214 forced drops reported ok=0, retried by the peer, 0 ACKed-but-undelivered in ~460k ok'd frames. With spsc-fat, the same stalls exhaust the pool and the pump drops the payload after chip admission+ACK: 5,468–5,739 ACKed-but-undelivered per run (4 replications, now counted by pool_dropped in rx.ring).
So spsc-fat's never-block invariant, implemented as drop-on-exhaustion, converts ARQ-recoverable loss into silent loss the TX peer logs as delivered. Users enable spsc-fat for smoothness (it eliminates async's retry storms during stalls) and silently trade away the ARQ contract at the exhaustion cliff.
Proposal
A pool-exhaustion policy: backpressure (new) vs drop (current).
In backpressure mode, on pool exhaustion the pump does not re-arm the completed URB — armed falls, the chip FIFO fills, the chip stops ACKing, and the loss becomes ARQ-visible exactly as in async mode — while the pump itself stays never-blocking and the consumer keeps its own thread. Absorption up to the pool size, honesty past it.
Steps
DeviceConfig::Rx::pool_exhaust (drop|backpressure) + DEVOURER_RX_POOL_EXHAUST in env_config. Default proposal: backpressure (contract-preserving, no-silent-caps); drop stays for latency-over-integrity consumers — maintainer call on the default.
UsbTransport spsc branch: on fresh == nullptr in backpressure mode, park the transfer (mutex-guarded stack) instead of re-arming with the received buffer; the consumer's buffer-return path re-arms parked transfers (libusb submit is thread-safe from any thread). Count pool_stalls instead of pool_dropped on this path.
- Telemetry:
rx.ring gains pool_stalls; docs/logging.md row updated; the spsc-fat mode comment's ARQ caveat updated to describe both policies.
- Teardown: parked transfers must be cancelled/reaped like armed ones — cover with
tests/teardown_gen_sanity.sh + tests/tx_teardown_asan.sh.
Validation / acceptance
tests/arq_e2e_delivery.sh escalation arm (DUT_SPIN_US=40 DUT_STALL_MS=150 DUT_STALL_EVERY=3000 DRONE_GAP_US=250 DUT_RX_MODE=spsc-fat) ×3 replications in backpressure mode: acked_undelivered = 0, pool_stalls > 0, drone-side ok% dips toward the async-arm profile (~93%) during stalls.
- No-stall baseline: au = 0 and throughput parity with drop mode.
- Drop mode unchanged and still counted (
pool_dropped).
Refs: #359 (bench + counter), docs/logging.md rx.ring row, CLAUDE.md ACK-horizon paragraph.
Context (measured, #359)
The arq_e2e bench proved both directions of the ACK contract on the 8812EU: with the default async ring, receiver-internal congestion (150 ms consumer freezes against a ~2.3 k fps flood) makes the chip decline the ACK — 14,214/14,214 forced drops reported
ok=0, retried by the peer, 0 ACKed-but-undelivered in ~460k ok'd frames. With spsc-fat, the same stalls exhaust the pool and the pump drops the payload after chip admission+ACK: 5,468–5,739 ACKed-but-undelivered per run (4 replications, now counted bypool_droppedinrx.ring).So spsc-fat's never-block invariant, implemented as drop-on-exhaustion, converts ARQ-recoverable loss into silent loss the TX peer logs as delivered. Users enable spsc-fat for smoothness (it eliminates async's retry storms during stalls) and silently trade away the ARQ contract at the exhaustion cliff.
Proposal
A pool-exhaustion policy:
backpressure(new) vsdrop(current).In backpressure mode, on pool exhaustion the pump does not re-arm the completed URB —
armedfalls, the chip FIFO fills, the chip stops ACKing, and the loss becomes ARQ-visible exactly as in async mode — while the pump itself stays never-blocking and the consumer keeps its own thread. Absorption up to the pool size, honesty past it.Steps
DeviceConfig::Rx::pool_exhaust(drop|backpressure) +DEVOURER_RX_POOL_EXHAUSTin env_config. Default proposal: backpressure (contract-preserving, no-silent-caps);dropstays for latency-over-integrity consumers — maintainer call on the default.UsbTransportspsc branch: onfresh == nullptrin backpressure mode, park the transfer (mutex-guarded stack) instead of re-arming with the received buffer; the consumer's buffer-return path re-arms parked transfers (libusb submit is thread-safe from any thread). Countpool_stallsinstead ofpool_droppedon this path.rx.ringgainspool_stalls;docs/logging.mdrow updated; the spsc-fat mode comment's ARQ caveat updated to describe both policies.tests/teardown_gen_sanity.sh+tests/tx_teardown_asan.sh.Validation / acceptance
tests/arq_e2e_delivery.shescalation arm (DUT_SPIN_US=40 DUT_STALL_MS=150 DUT_STALL_EVERY=3000 DRONE_GAP_US=250 DUT_RX_MODE=spsc-fat) ×3 replications in backpressure mode: acked_undelivered = 0,pool_stalls > 0, drone-side ok% dips toward the async-arm profile (~93%) during stalls.pool_dropped).Refs: #359 (bench + counter), docs/logging.md
rx.ringrow, CLAUDE.md ACK-horizon paragraph.