Date: 2026-08-09 Status: Accepted
The streamer UI wrote to three kinds of targets (Snapserver via sync WebSocket, CamillaDSP via sync WebSocket, local JSON DALs) using three different serialization mechanisms (asyncio.Lock, threading.Lock, event-loop-implicit), inconsistent error handling (blanket except Exception or nothing), and no backpressure on the volume slider. A source toggle could race with a second toggle, and a fast volume drag queued dozens of identical round-trips.
-
Three typed exceptions.
CommandError(base), withUnreachable,ServiceError, andStorageErrorsubclasses inaudera/errors.py. Every UI write handler catchesCommandError(plusRuntimeErrorfor@platform.requireson a dev box) instead ofException. -
Translation boundaries. Each client and service boundary catches raw exceptions and re-raises the typed equivalent:
Boundary Raw Typed SnapserverClient._callnetwork errors UnreachableSnapserverClient._call'error'in responseServiceErrorCamillaDSPClient._callnetwork errors UnreachableCamillaDSPClient._call'Error'/'Invalid'in responseServiceErrorCamillaDSPClient.validate_confignon- 'Ok'resultServiceErrorio.write_textOSErrorStorageErrorsystem.systemctlCalledProcessErrorServiceError(preserving stderr)system.systemctlTimeoutExpired,FileNotFoundErrorUnreachable -
platform.requiresstaysRuntimeError. It is a programming-environment guard, not a command failure.
-
One queue, one worker.
audera/ui/streamer/commands.pyholds aCommandQueuewith anasyncio.Queueand a single worker task. Every UI write path submits through it, so one command at a time reaches the target._CHOREOGRAPHY_LOCKis removed. -
Coalescing, not debouncing. When the worker is busy, pending commands with the same
coalesce_keycollapse to the latest. Replaced commands resolve withNone. No timers or artificial delays; the volume slider stays real-time. -
Volume coalescing key. The volume slider submits with
coalesce_key=('volume', client_id). A fast drag produces one write per worker cycle, not one per event. -
Readiness waits stay outside.
_await_snapserverpolls for up to 20 s after a source toggle. It runs outside the queue so it does not block other commands. -
DAL threading locks stay.
volume_dal._WRITE_LOCKandsources_dal._WRITE_LOCKare retained as defense-in-depth; the CLI also writes to these DALs. -
Singleton lifecycle.
commands.start()in_start()afterbroker.start().commands.stop()onapp.on_shutdownbeforebroker.stop().
- Every write handler narrows from
except Exceptiontoexcept CommandError, surfacing the right message instead of a stack trace or a silent swallow. - A source toggle that fails at
systemctl restartreports "is enabled, but applying it failed" and rolls forward, identical to the prior behavior but via the queue. - The volume slider on a slow CamillaDSP cannot queue unbounded writes; at most one pending write per player survives.
- The
_CHOREOGRAPHY_LOCKis gone. The queue serializes all writes inherently. - Client tests now assert
ServiceErrorandUnreachableinstead ofRuntimeError.