Compare commits

...
Author SHA1 Message Date
修雨 29abdb6e79 fix(event): serialize bus shutdown with accept loop
Wait for the accept loop to stop before waiting for connection handlers, and track accepted connections before publishing handlers. This removes the WaitGroup Add/Wait race caught by PR #667 CI and follows up the event bus introduced in #589.
2026-07-19 21:21:16 +08:00
修雨 6196e2565e Merge pull request #678 from DingTalk-Real-AI/fix/release-draft-asset-verify
fix(release): bind draft publication to release ID
2026-07-19 19:52:09 +08:00
2 changed files with 17 additions and 9 deletions
+4 -2
View File
@@ -377,9 +377,11 @@ func TestCrossPlatformCoverageDaemonMethodEdges(t *testing.T) {
d.conns.Store("not-a-conn", struct{}{})
d.conns.Store(&queryConn{}, struct{}{})
d.consumerWG.Add(1)
d.shutdown()
acceptDone := make(chan struct{})
close(acceptDone)
d.shutdown(acceptDone)
d.consumerWG.Done()
d.shutdown()
d.shutdown(acceptDone)
pr, pw, err := os.Pipe()
if err != nil {
+13 -7
View File
@@ -246,10 +246,11 @@ func Run(ctx context.Context, cfg Config) error {
}
// 7. Graceful shutdown — cancel runCtx first so all background
// goroutines wake up, then close listener / drain consumers.
// goroutines wake up, then stop accepting connections before draining
// consumers. The accept-loop barrier is required before WaitGroup.Wait:
// sync.WaitGroup forbids a positive Add racing with Wait.
cancelRun()
d.shutdown()
<-acceptDone
d.shutdown(acceptDone)
<-idleDone
<-dropWarnDone
@@ -291,6 +292,10 @@ func (d *daemon) acceptLoop(ctx context.Context) {
d.log.Warn("bus: accept error", "err", err)
continue
}
// Track the connection before publishing the handler goroutine. Once
// acceptLoop returns, shutdown can therefore close every accepted
// connection before waiting for handlers to drain.
d.conns.Store(conn, struct{}{})
d.consumerWG.Add(1)
go func() {
defer d.consumerWG.Done()
@@ -303,7 +308,6 @@ func (d *daemon) acceptLoop(ctx context.Context) {
// Hello → register with Hub → spawn writer goroutine → read until EOF/Bye.
// Always Unregisters and Closes on exit (plan invariant #5).
func (d *daemon) handleConnection(ctx context.Context, conn net.Conn) {
d.conns.Store(conn, struct{}{})
defer func() {
d.conns.Delete(conn)
conn.Close()
@@ -478,9 +482,10 @@ func (d *daemon) triggerShutdown(reason string) {
// 1. mark shuttingDown so acceptLoop exits cleanly
// 2. broadcast Bye to all consumers
// 3. close listener (interrupts pending Accept)
// 4. wait for all per-connection goroutines to drain
// 5. lock + meta cleanup via Run's defers
func (d *daemon) shutdown() {
// 4. wait for acceptLoop to return so no future consumerWG.Add can occur
// 5. close all accepted connections and wait for handlers to drain
// 6. lock + meta cleanup via Run's defers
func (d *daemon) shutdown(acceptDone <-chan struct{}) {
d.shutdownMu.Lock()
defer d.shutdownMu.Unlock()
if !d.shuttingDown.CompareAndSwap(false, true) {
@@ -488,6 +493,7 @@ func (d *daemon) shutdown() {
}
d.hub.Broadcast(transport.Bye{Type: transport.FrameTypeBye, Reason: "shutdown"})
_ = d.listener.Close()
<-acceptDone
// Force-close all open IPC connections so any reader goroutine blocked
// on Read() returns with a network error and exits cleanly. Without
// this the consumerWG never drains and Run hangs forever.