From 189d62e7f7528c778ea4e1cae6f1e1240c7ee521 Mon Sep 17 00:00:00 2001 From: "Matthew (bot)" Date: Tue, 21 Jul 2026 10:38:51 +0000 Subject: [PATCH] fix(daemon): register echo + IPC bind accept loops on bgWG (PILOT-88) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two long-lived fire-and-forget goroutines — the echo service accept loop (startEchoService) and the IPC bind accept pusher (handleBind) — were spawned without registering on d.bgWG. During graceful shutdown, these goroutines check stopCh or rely on AcceptCh close to exit, but bgWG.Wait() in doStop had no visibility into them; only the 5s wall-clock timeout protected against shutdown races with shared state. Fix: add d.bgWG.Add(1) / defer d.bgWG.Done() around both accept loops so doStop tracks them alongside the existing 14 tracked background goroutines (routeLoop, trustRepublishLoop, etc.). Test: TestStartEchoServiceBGWaitGroupTracksAcceptLoop verifies that close(stopCh) drains bgWG within 2s. Closes PILOT-88 --- pkg/daemon/ipc.go | 2 ++ pkg/daemon/services.go | 2 ++ pkg/daemon/zz_services_bootstrap_test.go | 33 ++++++++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/pkg/daemon/ipc.go b/pkg/daemon/ipc.go index 6e40cedf..fa1400e5 100644 --- a/pkg/daemon/ipc.go +++ b/pkg/daemon/ipc.go @@ -876,7 +876,9 @@ func (s *IPCServer) handleBind(conn *ipcConn, reqID uint64, payload []byte) { // Start pushing accepted connections to this client. CmdAccept frames // are server-pushed (reqID=0) and demuxed by the driver on local port. + s.daemon.bgWG.Add(1) go func() { + defer s.daemon.bgWG.Done() for c := range ln.AcceptCh { conn.trackConn(c.ID) // H12 fix: include local port for per-port demux diff --git a/pkg/daemon/services.go b/pkg/daemon/services.go index eaa7b316..1cd4ec81 100644 --- a/pkg/daemon/services.go +++ b/pkg/daemon/services.go @@ -145,7 +145,9 @@ func (d *Daemon) startEchoService() error { if err != nil { return err } + d.bgWG.Add(1) go func() { + defer d.bgWG.Done() for { select { case conn, ok := <-ln.AcceptCh: diff --git a/pkg/daemon/zz_services_bootstrap_test.go b/pkg/daemon/zz_services_bootstrap_test.go index a80482d7..1e064c0f 100644 --- a/pkg/daemon/zz_services_bootstrap_test.go +++ b/pkg/daemon/zz_services_bootstrap_test.go @@ -179,3 +179,36 @@ func TestStartEchoServiceAcceptLoopExitsOnUnbind(t *testing.T) { // detached goroutine but Sleep gives it a chance to drain. time.Sleep(100 * time.Millisecond) } + +// --- startEchoService increments bgWG and stopCh drains it --- + +func TestStartEchoServiceBGWaitGroupTracksAcceptLoop(t *testing.T) { + t.Parallel() + d := New(Config{}) + + if err := d.startEchoService(); err != nil { + t.Fatalf("startEchoService: %v", err) + } + + // Close stopCh — accept loop should exit via <-d.stopCh and + // defer d.bgWG.Done() fires, draining the WaitGroup. + close(d.stopCh) + + if !wgDrained(&d.bgWG, 2*time.Second) { + t.Fatal("bgWG not drained within 2s after stopCh close") + } +} + +func wgDrained(wg *sync.WaitGroup, timeout time.Duration) bool { + ch := make(chan struct{}) + go func() { + wg.Wait() + close(ch) + }() + select { + case <-ch: + return true + case <-time.After(timeout): + return false + } +}