Skip to content

Commit 1c749ee

Browse files
committed
Record item 28 as shipped
1 parent 19f6ea0 commit 1c749ee

1 file changed

Lines changed: 42 additions & 0 deletions

File tree

docs/ROADMAP.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2485,3 +2485,45 @@ hand-entered address is neither, so it went.
24852485
credential-redaction check went with them.
24862486
- **`SENDSPIN_SERVER_URL` is answered only for the server_id the dial chose.** `LastDial` has
24872487
no "literal URL, taken at its word" case left; a dial with no id answers nothing.
2488+
2489+
### 28. A second device outage in one stream never recovers — *shipped (hardware pass still owed)*
2490+
2491+
Found by item 27's CoreAudio hardware pass — two unplugs in one run, the first back in 190 ms,
2492+
the second never — but it belongs to item 14 and sits in the shared `SinkRecovery`, so every
2493+
device-backed sink had it. **A rescan that succeeded retired the budget instead of restoring
2494+
it**: `rescan_done()` sent a recovered rescan and an exhausted one down the same branch, and
2495+
`reopen_spent_` was still set from the first outage. On the second, `reopen_due()` declined,
2496+
`escalate_()` found the rescan spent, and the sink discarded until the next track with no log
2497+
line at all.
2498+
2499+
**Shipped** in `src/sink_recovery.{h,cpp}`, its callers (`src/alsa_sink.cpp`,
2500+
`src/pulse_sink.cpp`, `src/pipewire_sink.cpp`, `src/portaudio_sink.cpp`) and
2501+
`tests/sink_recovery_test.cpp`:
2502+
2503+
- **The budget is per outage, not per stream.** `rescan_done(true)` now refills it — the
2504+
in-place reopen and the whole rescan ladder, back to `SINK_RESCAN_DELAY_MS` — the same intent
2505+
`reopen_done(true)` already had for its own path.
2506+
- **The outage gap survives the refill.** Only the attempt bookkeeping refills; the
2507+
discarded-frame count is not `reset()`'s to drop here, since the first timed write still owes
2508+
the player that gap, and a device that dies again before one adds to it.
2509+
- **A device that never comes back still gives up.** Within one outage the budget is unchanged —
2510+
one reopen, then `SINK_RESCAN_ATTEMPTS` rescans on the doubling delay — and only a real
2511+
recovery refills it.
2512+
- **`rescan_abandoned()` for an attempt that did not recover anything.** The shutdown paths in
2513+
ALSA, PulseAudio and PipeWire used to report `rescan_done(true)` when `stop()` landed
2514+
mid-attempt, which would now refill; they abandon instead. PortAudio used to report `true` up
2515+
front whatever happened; it now abandons on each failure and reports `true` only when the
2516+
stream is really back, so its rebuild stays one-shot per outage.
2517+
2518+
**A flapping device is not given a floor of its own.** One that recovers and dies again,
2519+
repeatedly, now keeps recovering — which is what it is doing — and the loop is already paced:
2520+
each outage spends the in-place reopen at most once, the next one has to wait out
2521+
`SINK_RESCAN_DELAY_MS` before its rescan, and a refill never shortens that below the base delay.
2522+
So the main loop pays at most one rescan per two seconds whatever the device does. A cap on
2523+
recoveries per stream would turn a flaky cable back into the silence this item removes.
2524+
2525+
The regression tests (`ASecondOutageInTheSameStreamRecovers`,
2526+
`EveryOutageAfterARecoveryStillGivesUp`, `TheDiscardedGapSurvivesTheRefillIntoTheNextOutage`)
2527+
fail against the old helper and pass against the new one. **Still owed:** two unplug/replug
2528+
cycles in one stream on real hardware — ALSA first, since it has the most users, then CoreAudio
2529+
with item 27's harness.

0 commit comments

Comments
 (0)