shutdown: close the sockets, or the database never gets sealed
mavend seals its encrypted database in `defer st.Close()` when run() returns. It had not returned since 2026-07-21. Every restart since then decrypted the same eleven-day-old ciphertext and rolled back everything written in between: the Telegram nudge that kept firing was a fact being un-written on each boot. The goroutine dump named it. main → srv.Close() → ipc.(*Server).Close → wg.Wait(), waiting on per-connection goroutines parked in readFrame. Close shut the listener and nothing else, so the idle persistent sockets held by mavweb, mavpoll, mavcaldav and mavmaild blocked shutdown forever. `docker compose stop -t 60` spent the whole sixty seconds and then took a SIGKILL. So: track the accepted conns and close them, in ipc and in voice, which had the identical defect. Bound all three waits — the two per-server ones and the worker wait in main — because the seal matters more than any single in-flight call. A dropped RPC costs one reply; a missed seal costs a session. The regression test leaves a client connected and idle, which is the case the old tests avoided by closing the client first. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TrVSBKe3RFDF4fGYKWYQnX
This commit is contained in:
@@ -640,3 +640,30 @@ func TestSwapModel_Hook(t *testing.T) {
|
||||
t.Fatalf("swap to a non-allowlisted path = %v; want ErrForbidden", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The eleven-day bug: Close shut the listener but not the accepted conns, so
|
||||
// an idle client left serveConn parked in readFrame and wg.Wait never
|
||||
// returned. mavend deadlocked before `defer st.Close()` could re-encrypt the
|
||||
// database, and every write since the last clean stop was rolled back on the
|
||||
// next boot. The client here stays connected and idle on purpose.
|
||||
func TestCloseReturnsWithAnIdleClientConnected(t *testing.T) {
|
||||
_, srv, cli, _ := newServerWithStore(t)
|
||||
// Prove the conn is live and then leave it alone — no cli.Close().
|
||||
if _, err := cli.RecentFacts(context.Background(), 1); err != nil {
|
||||
t.Fatalf("warm-up call: %v", err)
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- srv.Close() }()
|
||||
select {
|
||||
case err := <-done:
|
||||
if err != nil {
|
||||
t.Fatalf("close: %v", err)
|
||||
}
|
||||
// Deliberately shorter than closeGrace: the grace timer is the backstop,
|
||||
// not the mechanism. Closing the conns is what makes readFrame return, and
|
||||
// if that regresses this waits out the full grace and fails here.
|
||||
case <-time.After(closeGrace / 2):
|
||||
t.Fatal("Close blocked on an idle connection — the shutdown deadlock is back")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user