mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +00:00
Status was a value-typed DeviceStatus field on DeviceConnection,
written from the periodic poller (UpdateDeviceStatus) and from four
WebSocket event handlers (OnNowPlaying, OnVolumeUpdated,
OnConnectionState, OnPresetUpdated) while being read from every HTTP
handler and the WebSocket broadcaster. The struct was 8+ words wide
with time.Time and string members, so concurrent readers could
observe torn fields or mixed-update snapshots. The map-level race was
fixed in the previous commit; this one closes the per-connection
struct race.
Hide the field behind atomic.Pointer[DeviceStatus]:
Status() // returns current snapshot
SetStatus(*DeviceStatus) // wholesale replace
UpdateStatus(func(*DeviceStatus)) // CAS retry loop
NewDeviceConnection constructs a connection with the atomic pointer
pre-initialised, so Status() never returns nil for callers that go
through the constructor (the old struct-literal pattern is no longer
possible because the status field is now private).
UpdateDeviceStatus runs network fetches into local vars first, then
batches them into a single UpdateStatus call so the CAS loop only
retries the merge — not the slow IO. WebSocket event handlers and
the connect/disconnect transitions each use UpdateStatus, so any
ordering of poller + event delivery converges to a consistent
status.
The UpdateStatus docstring is explicit about the shallow-copy
contract: nested pointer fields (NowPlaying, Volume, Bass, Presets,
Sources) MUST be replaced, not mutated through, because the copy
mut receives shares those pointers with the prior snapshot. All
production callers already follow this pattern (every value comes
fresh from the device API).
Tests:
- types_test.go: migrated literal struct to NewDeviceConnection +
SetStatus, switched reads to Status().
- status_test.go (new): six tests covering constructor init,
SetStatus replacement semantics, UpdateStatus mutator
application, field preservation across UpdateStatus, snapshot
isolation (old snapshot stable under later writes), and a
concurrent stress test (16 writers + 32 readers x 200 ops) that
runs under -race.
- handlers_test.go, registry_test.go, spa_test.go: migrated to
constructor.
Not addressed by this commit:
- DeviceConnection.WebSocket (set once in ConnectDeviceWebSocket,
read elsewhere). Word-sized pointer, atomic at the hardware
level on amd64/arm64; race detector may still flag.
- DeviceConnection.LastSeen (written under devicesMu by the
registry, read outside that lock via DeviceSnapshot consumers).
time.Time is non-atomic but the read is cosmetic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>