@@ -89,6 +89,12 @@ type Server struct {
8989 // signal handler in run.go both call Stop(), so without this guard
9090 // components, databases, and the health exporter would be closed twice.
9191 stopOnce sync.Once
92+ loopWG sync.WaitGroup
93+
94+ // loopCtx/loopCancel control background inventory and attestation goroutines.
95+ // Stop() cancels this context and waits on loopWG for graceful shutdown.
96+ loopCtx context.Context
97+ loopCancel context.CancelFunc
9298
9399 machineID string
94100}
@@ -210,6 +216,24 @@ func getAttestationTimeout(cfg *config.Config) time.Duration {
210216 return config .DefaultAttestationTimeout
211217}
212218
219+ func waitForWaitGroup (wg * sync.WaitGroup , timeout time.Duration ) bool {
220+ done := make (chan struct {})
221+ go func () {
222+ wg .Wait ()
223+ close (done )
224+ }()
225+ if timeout <= 0 {
226+ <- done
227+ return true
228+ }
229+ select {
230+ case <- done :
231+ return true
232+ case <- time .After (timeout ):
233+ return false
234+ }
235+ }
236+
213237// shouldEnableComponent determines if a component should be enabled based on configuration
214238func shouldEnableComponent (name string , enabledByDefault bool , config * config.Config ) bool {
215239 shouldEnable := enabledByDefault
@@ -235,11 +259,14 @@ func New(ctx context.Context, auditLogger log.AuditLogger, config *config.Config
235259 return nil , err
236260 }
237261
262+ loopCtx , loopCancel := context .WithCancel (ctx )
238263 s := & Server {
239264 auditLogger : auditLogger ,
240265 dbRW : dbRW ,
241266 dbRO : dbRO ,
242267 config : config ,
268+ loopCtx : loopCtx ,
269+ loopCancel : loopCancel ,
243270 }
244271 defer func () {
245272 if retErr != nil {
@@ -383,8 +410,8 @@ func New(ctx context.Context, auditLogger log.AuditLogger, config *config.Config
383410 }
384411 }
385412
386- s .startInventoryLoop (ctx , config , nvmlInstance , dcgmGPUIndexes )
387- s .startAttestationLoop (ctx , config )
413+ s .startInventoryLoop (loopCtx , config , nvmlInstance , dcgmGPUIndexes )
414+ s .startAttestationLoop (loopCtx , config )
388415
389416 // Create and start health exporter with all dependencies if enabled
390417 if config .HealthExporter != nil {
@@ -462,7 +489,9 @@ func (s *Server) startInventoryLoop(
462489 StartupJitter : inventory .DefaultStartupJitter ,
463490 })
464491
492+ s .loopWG .Add (1 )
465493 go func () {
494+ defer s .loopWG .Done ()
466495 if err := manager .Run (ctx ); err != nil && ! errors .Is (err , context .Canceled ) {
467496 log .Logger .Errorw ("inventory loop manager exited" , "error" , err )
468497 }
@@ -497,7 +526,9 @@ func (s *Server) startAttestationLoop(ctx context.Context, cfg *config.Config) {
497526 },
498527 )
499528
529+ s .loopWG .Add (1 )
500530 go func () {
531+ defer s .loopWG .Done ()
501532 if err := manager .Run (ctx ); err != nil && ! errors .Is (err , context .Canceled ) {
502533 log .Logger .Errorw ("attestation loop exited" , "error" , err )
503534 }
@@ -516,6 +547,15 @@ func (s *Server) GetHealthExporter() exporter.Exporter {
516547// signal handler in run.go both invoke it.
517548func (s * Server ) Stop () {
518549 s .stopOnce .Do (func () {
550+ // Signal inventory/attestation loops to stop and wait for graceful exit
551+ // before we close dependencies they may still be using.
552+ if s .loopCancel != nil {
553+ s .loopCancel ()
554+ }
555+ if ! waitForWaitGroup (& s .loopWG , 10 * time .Second ) {
556+ log .Logger .Warnw ("timed out waiting for background loops to stop" )
557+ }
558+
519559 // Gracefully shut down the HTTP server so in-flight requests complete
520560 // before we close databases and components underneath them.
521561 if s .srv != nil {
0 commit comments