server: serialize DrainPending per host (avoid drain double-dispatch)
Add a per-host drain mutex (drainLocks map guarded by drainLocksMu) on the Server struct. DrainPending acquires it with TryLock: if a drain is already in-flight for this host, the call returns immediately — the running drain will see every pending row. This prevents the on-hello goroutine and the 30s tick from both listing the same host's rows and dispatching them twice. Update three existing tests that called srv.DrainPending explicitly after the on-hello goroutine had already been spawned: replace the now-redundant direct call with a waitForPendingCount poll so they don't race the goroutine's mutex ownership. Add TestDrainPendingSerializesPerHost which fires 10 concurrent DrainPending goroutines against a 5-row queue and asserts exactly 5 job rows result.
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
stdhttp "net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
@@ -41,6 +42,13 @@ type Deps struct {
|
||||
type Server struct {
|
||||
srv *stdhttp.Server
|
||||
deps Deps
|
||||
|
||||
// drainLocks serializes DrainPending per host. The on-hello
|
||||
// goroutine and the 30s ticker can otherwise race for the same
|
||||
// host, double-dispatching every pending row. Map of hostID →
|
||||
// sync.Mutex; checked-and-locked atomically via drainLocksMu.
|
||||
drainLocksMu sync.Mutex
|
||||
drainLocks map[string]*sync.Mutex
|
||||
}
|
||||
|
||||
// New builds a configured but not-yet-started server.
|
||||
@@ -59,7 +67,7 @@ func New(deps Deps) *Server {
|
||||
w.WriteHeader(stdhttp.StatusNoContent)
|
||||
})
|
||||
|
||||
s := &Server{deps: deps}
|
||||
s := &Server{deps: deps, drainLocks: make(map[string]*sync.Mutex)}
|
||||
s.routes(r)
|
||||
|
||||
s.srv = &stdhttp.Server{
|
||||
|
||||
Reference in New Issue
Block a user