{"record":{"id":"e9e15eb4e38a47b1","repo":"slackhq/nebula","slug":"queue-set-already-closed","errorCode":null,"errorMessage":"queue set already closed","messagePattern":"queue set already closed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"overlay/tio/queueset_gso_linux.go","lineNumber":54,"sourceCode":"\n\tout := &offloadQueueSet{\n\t\tpq:         []*Offload{},\n\t\tpqi:        []Queue{},\n\t\tshutdownFd: shutdownFd,\n\t\tusoEnabled: usoEnabled,\n\t\tl:          l,\n\t}\n\n\treturn out, nil\n}\n\nfunc (c *offloadQueueSet) Queues() []Queue {\n\treturn c.pqi\n}\n\nfunc (c *offloadQueueSet) Add(fd int) error {\n\tif c.closed.Load() {\n\t\treturn errors.New(\"queue set already closed\")\n\t}\n\tx, err := newOffload(fd, c.shutdownFd, c.usoEnabled, c.l)\n\tif err != nil {\n\t\treturn err\n\t}\n\tc.pq = append(c.pq, x)\n\tc.pqi = append(c.pqi, x)\n\n\treturn nil\n}\n\nfunc (c *offloadQueueSet) wakeForShutdown() error {\n\tvar buf [8]byte\n\tbinary.NativeEndian.PutUint64(buf[:], 1)\n\t_, err := unix.Write(c.shutdownFd, buf[:])\n\treturn err\n}\n","sourceCodeStart":36,"sourceCodeEnd":72,"githubUrl":"https://github.com/slackhq/nebula/blob/dd8f660c0ac37903ec4080ca4d3c861ba9342ceb/overlay/tio/queueset_gso_linux.go#L36-L72","documentation":"The GSO/offload queue set (offloadQueueSet) returns a fresh errors.New(\"queue set already closed\") from Add when its closed atomic flag is set, meaning the queue set has been shut down and no new file descriptors may be attached as offload queues. It is a lifecycle guard preventing adds after Close.","triggerScenarios":"Calling offloadQueueSet.Add(fd) after the queue set's Close/shutdown path has flipped c.closed. Typical when socket registration races with shutdown: a new connection's fd is handed to Add while another goroutine is tearing the queue set down.","commonSituations":"Server shutdown while connections are still being accepted; listener reconfiguration that closes and recreates queue sets; fd registration racing with process-level teardown; test harnesses closing the set before draining pending adds.","solutions":["Check queue set state (or a exposed closed flag) before calling Add; skip adding fds for a closed set.","Serialize shutdown and fd registration so Add cannot run concurrently with Close (mutex or single-owner goroutine).","Make Close wait for in-flight Add calls (WaitGroup) so the race window disappears.","Handle the error gracefully in the caller: drop/close the fd, since the queue set is going away anyway.","Aggregate a sentinel error so callers can errors.Is-compare instead of string matching."],"exampleFix":"// before: string-matching an ad-hoc error\nif err := qs.Add(fd); err != nil {\n    if err.Error() == \"queue set already closed\" { ... }\n}\n// after: check state before adding\nif qs.Closed() {\n    fd.Close()\n    return\n}\nif err := qs.Add(fd); err != nil {\n    fd.Close()\n}","handlingStrategy":"try-catch","validationCode":"// Go: check lifecycle state before Add\nif qs.Closed() {\n    fd.Close()\n    return\n}","typeGuard":null,"tryCatchPattern":"if err := qs.Add(fd); err != nil {\n    if strings.Contains(err.Error(), \"already closed\") || qs.Closed() {\n        fd.Close()\n        return nil // benign during shutdown\n    }\n    return err\n}","preventionTips":["Expose an IsClosed()/Closed() check and consult it before every Add.","Use a sentinel error (var ErrClosed = errors.New(...)) so errors.Is works reliably.","Synchronize Add and Close with a mutex or WaitGroup to eliminate the race.","During shutdown, close fds in the Add caller rather than registering them.","Log shutdown races explicitly to find accept-loop/teardown ordering bugs."],"tags":["linux","network","gso","lifecycle","concurrency"],"backgroundTag":"queue-set-closed","analyzedSha":"dd8f660c0ac37903ec4080ca4d3c861ba9342ceb","analyzedAt":"2026-09-03T11:13:55.444Z","contentChangedAt":"2026-09-03T11:13:55.444Z","schemaVersion":2},"datasetVersion":"2026-09-10T17:17:09.494Z"}