@@ -654,27 +654,36 @@ func (r *Runtime) Close() error {
654654 }
655655
656656 // Spec 080 (US3, FR-010): resolve the shutdown marker to "clean" at the
657- // LAST point the DB is still open — after cache/index cleanup, immediately
658- // before storage closes (cache.Close only stops a goroutine and index.Close
659- // only touches Bleve; neither closes the BBolt DB). Every branch above —
660- // including the container-cleanup verification, whose early exits live
661- // inside verifyContainerCleanup — reaches this point, so a graceful Close
662- // always resolves; conversely a hang/SIGKILL/panic ANYWHERE earlier in
663- // shutdown leaves the marker armed and the next instance honestly reports
664- // previous_shutdown="crash". Idempotent on double Close; on an
665- // already-closed DB the write fails harmlessly (logged at debug) and the
666- // marker is left untouched.
667- if r .prechurnStore != nil && r .storageManager != nil {
668- if db := r .storageManager .GetDB (); db != nil {
669- if err := r .prechurnStore .ResolveCleanShutdown (db ); err != nil {
670- if r .logger != nil {
671- r .logger .Debug ("Failed to resolve shutdown marker to clean" , zap .Error (err ))
657+ // LAST point the DB is still open — i.e. after the async storage manager
658+ // has stopped AND drained its queue (those queued operations perform BBolt
659+ // writes; StopAsync below runs that drain), immediately before the BBolt
660+ // handle closes. Every branch above — including the container-cleanup
661+ // verification, whose early exits live inside verifyContainerCleanup —
662+ // reaches this point, so a graceful Close always resolves; conversely a
663+ // hang/SIGKILL/panic ANYWHERE earlier in shutdown — including mid-drain —
664+ // leaves the marker armed and the next instance honestly reports
665+ // previous_shutdown="crash". Idempotent on double Close (StopAsync no-ops,
666+ // and storageManager.Close's internal async stop no-ops after StopAsync);
667+ // on an already-closed DB the marker write fails harmlessly (logged at
668+ // debug) and the marker is left untouched.
669+ if r .storageManager != nil {
670+ // (1) Stop + drain queued async DB operations — the last DB writes
671+ // other than the marker resolve itself.
672+ r .storageManager .StopAsync ()
673+
674+ // (2) Resolve the marker to clean, now that no other DB work remains.
675+ if r .prechurnStore != nil {
676+ if db := r .storageManager .GetDB (); db != nil {
677+ if err := r .prechurnStore .ResolveCleanShutdown (db ); err != nil {
678+ if r .logger != nil {
679+ r .logger .Debug ("Failed to resolve shutdown marker to clean" , zap .Error (err ))
680+ }
672681 }
673682 }
674683 }
675- }
676684
677- if r .storageManager != nil {
685+ // (3) Close the BBolt handle; the async stop inside Close is a no-op,
686+ // so no DB work intervenes between the marker resolve and db.Close.
678687 if err := r .storageManager .Close (); err != nil {
679688 errs = append (errs , fmt .Errorf ("close storage manager: %w" , err ))
680689 }
0 commit comments