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
()
| 355 | // Store.Close() so in-flight DB writes don't race a closed connection |
| 356 | // (or worse, the SQLite -wal/-shm file removal in t.TempDir cleanup). |
| 357 | func (s *Server) Stop() { |
| 358 | // Signal long-running background loops (orphan GC, etc.) to exit. |
| 359 | // Each loop registers itself on s.bg, so the Wait() below blocks |
| 360 | // until they actually finish and any in-flight goroutines drain. |
| 361 | s.stopOrphanGC() |
| 362 | // Yjs op-log prune sweeper (TASK-1309). Same lifecycle pattern; |
| 363 | // signals BEFORE Wait() so the goroutine sees the close and exits. |
| 364 | s.stopOpLogGC() |
| 365 | // Short-lived-credential reaper (PLAN-1933 DR-5 / TASK-1936). Same |
| 366 | // lifecycle pattern; signal BEFORE Wait() so the goroutine exits. |
| 367 | s.stopTokenReaper() |
| 368 | // Soft-deleted-workspace hard-purge sweeper (TASK-1966). Same |
| 369 | // lifecycle pattern; signal BEFORE Wait() so the goroutine exits. |
| 370 | s.stopWorkspacePurgeSweeper() |
| 371 | // MCP audit writer / sweeper run on s.bg too. Signal first so |
| 372 | // the workers see the close BEFORE Wait() blocks; without the |
| 373 | // signal Wait would hang forever on the writer's blocking |
| 374 | // queue receive. |
| 375 | s.stopMCPAuditWriter() |
| 376 | // MCP session tracker (TASK-1120) runs its sweeper on s.bg too. |
| 377 | // Order with the audit writer doesn't matter — both are |
| 378 | // independent goroutines; we just need the close BEFORE Wait(). |
| 379 | s.stopMCPSessionTracker() |
| 380 | // Close the collab room manager BEFORE bg.Wait() so any in-flight |
| 381 | // op-log GC sweep (TASK-1309) blocked on a per-item lock behind |
| 382 | // an active Join can drain. collab.Close() tears down the Joins |
| 383 | // (their WS readLoops return, runConn unwinds, itemLocks |
| 384 | // release), which unblocks the GC's per-item PruneItemOpLogIfDormantBefore |
| 385 | // call. Without this ordering, Stop() can deadlock: GC waits on |
| 386 | // itemLock; Join holds itemLock until WS closes; WS only closes |
| 387 | // when collab.Close() runs; collab.Close() only runs after |
| 388 | // bg.Wait(); bg.Wait() never returns because GC is stuck. |
| 389 | // Per Codex review of TASK-1309 [P2]. nil-safe: collab is optional. |
| 390 | if s.collab != nil { |
| 391 | s.collab.Close() |
| 392 | } |
| 393 | s.bg.Wait() |
| 394 | s.rateLimiters.Stop() // nil-safe via the RateLimiters receiver guard |
| 395 | } |
| 396 | |
| 397 | func New(s *store.Store) *Server { |
| 398 | rl := NewRateLimiters() |