Stop waits for all background goroutines started via goAsync to finish AND drains the rate-limiter cleanup goroutines spawned at construction time (BUG-851). Safe to call multiple times. Should be called before Store.Close() so in-flight DB writes don't race a closed connection (or worse, the SQLite
()
| 311 | // Store.Close() so in-flight DB writes don't race a closed connection |
| 312 | // (or worse, the SQLite -wal/-shm file removal in t.TempDir cleanup). |
| 313 | func (s *Server) Stop() { |
| 314 | // Signal long-running background loops (orphan GC, etc.) to exit. |
| 315 | // Each loop registers itself on s.bg, so the Wait() below blocks |
| 316 | // until they actually finish and any in-flight goroutines drain. |
| 317 | s.stopOrphanGC() |
| 318 | // Yjs op-log prune sweeper (TASK-1309). Same lifecycle pattern; |
| 319 | // signals BEFORE Wait() so the goroutine sees the close and exits. |
| 320 | s.stopOpLogGC() |
| 321 | // Short-lived-credential reaper (PLAN-1933 DR-5 / TASK-1936). Same |
| 322 | // lifecycle pattern; signal BEFORE Wait() so the goroutine exits. |
| 323 | s.stopTokenReaper() |
| 324 | // Soft-deleted-workspace hard-purge sweeper (TASK-1966). Same |
| 325 | // lifecycle pattern; signal BEFORE Wait() so the goroutine exits. |
| 326 | s.stopWorkspacePurgeSweeper() |
| 327 | // MCP audit writer / sweeper run on s.bg too. Signal first so |
| 328 | // the workers see the close BEFORE Wait() blocks; without the |
| 329 | // signal Wait would hang forever on the writer's blocking |
| 330 | // queue receive. |
| 331 | s.stopMCPAuditWriter() |
| 332 | // MCP session tracker (TASK-1120) runs its sweeper on s.bg too. |
| 333 | // Order with the audit writer doesn't matter — both are |
| 334 | // independent goroutines; we just need the close BEFORE Wait(). |
| 335 | s.stopMCPSessionTracker() |
| 336 | // Close the collab room manager BEFORE bg.Wait() so any in-flight |
| 337 | // op-log GC sweep (TASK-1309) blocked on a per-item lock behind |
| 338 | // an active Join can drain. collab.Close() tears down the Joins |
| 339 | // (their WS readLoops return, runConn unwinds, itemLocks |
| 340 | // release), which unblocks the GC's per-item PruneItemOpLogIfDormantBefore |
| 341 | // call. Without this ordering, Stop() can deadlock: GC waits on |
| 342 | // itemLock; Join holds itemLock until WS closes; WS only closes |
| 343 | // when collab.Close() runs; collab.Close() only runs after |
| 344 | // bg.Wait(); bg.Wait() never returns because GC is stuck. |
| 345 | // Per Codex review of TASK-1309 [P2]. nil-safe: collab is optional. |
| 346 | if s.collab != nil { |
| 347 | s.collab.Close() |
| 348 | } |
| 349 | s.bg.Wait() |
| 350 | s.rateLimiters.Stop() // nil-safe via the RateLimiters receiver guard |
| 351 | } |
| 352 | |
| 353 | func New(s *store.Store) *Server { |
| 354 | rl := NewRateLimiters() |