Document websocket lifecycle synchronization

This commit is contained in:
courtmanr@gmail.com
2026-07-26 22:02:22 +01:00
parent 27ba37982e
commit fe33d23210
2 changed files with 48 additions and 3 deletions
@@ -268,9 +268,13 @@ initial-client delivery, and explicit client data requests; cancel delayed or
queued work when its client leaves; and join every state producer before
closing client channels. Reconnect, request, or invalidation churn must not
multiply concurrent clones of that canonical payload or race shutdown sends
against channel closure. Ordinary unregister and slow-client eviction must use
the same synchronized send/close ownership; recovering a send-on-closed panic
is not a substitute for a race-free channel lifecycle.
against channel closure. A client's lifecycle cancellation signal must be
initialized and read through one synchronized owner before registration,
requested-state work, delayed initial-state delivery, or disconnect may race
over it; every path must observe the same stable signal. Ordinary unregister
and slow-client eviction must use the same synchronized send/close ownership;
recovering a send-on-closed panic is not a substitute for a race-free channel
lifecycle.
Docker and Podman app-container CPU payloads expose two API facts with
different meanings: canonical resource metrics and `/api/metrics-store/history`
+41
View File
@@ -35,6 +35,47 @@ func TestClientSafeSendAndCloseAreSynchronized(t *testing.T) {
}
}
func TestClientLifecycleSignalConcurrentInitializationAndClose(t *testing.T) {
client := &Client{}
start := make(chan struct{})
signals := make([]chan struct{}, 32)
var workers sync.WaitGroup
for i := range signals {
workers.Add(1)
go func(index int) {
defer workers.Done()
<-start
signals[index] = client.lifecycleSignal()
}(i)
}
for i := 0; i < 8; i++ {
workers.Add(1)
go func() {
defer workers.Done()
<-start
client.closeLifecycle()
}()
}
close(start)
workers.Wait()
for i, signal := range signals {
if signal == nil {
t.Fatalf("lifecycle signal %d was nil", i)
}
if signal != signals[0] {
t.Fatalf("lifecycle signal %d did not share the client lifecycle", i)
}
}
select {
case <-signals[0]:
default:
t.Fatal("lifecycle signal remained open after closeLifecycle")
}
}
func TestHubDisconnectedClientsDoNotBuildInitialState(t *testing.T) {
var stateBuilds atomic.Int64
hub := NewHub(func(string) interface{} {