goAsync spawns fn in a goroutine that's tracked by s.bg, so Stop() can wait for in-flight background work to finish. Use this for any fire-and-forget work that touches the database, filesystem, or external services from inside a request handler — never bare `go func() {...}()`.
(fn func())
| 262 | // fire-and-forget work that touches the database, filesystem, or external |
| 263 | // services from inside a request handler — never bare `go func() {...}()`. |
| 264 | func (s *Server) goAsync(fn func()) { |
| 265 | s.bg.Add(1) |
| 266 | go func() { |
| 267 | defer s.bg.Done() |
| 268 | // Recover from panics in fn so a single bad background task |
| 269 | // (e.g. deriveThumbnails hitting a Go image-decoder panic on a |
| 270 | // crafted upload, or an email send) can't crash the whole |
| 271 | // single-binary server for every tenant. chi's Recoverer only |
| 272 | // covers request goroutines, not these detached ones. The |
| 273 | // deferred Done() above still fires because recover() keeps the |
| 274 | // goroutine from unwinding past this point. |
| 275 | defer func() { |
| 276 | if r := recover(); r != nil { |
| 277 | slog.Error("background task panicked", |
| 278 | "panic", r, |
| 279 | "stack", string(debug.Stack())) |
| 280 | } |
| 281 | }() |
| 282 | fn() |
| 283 | }() |
| 284 | } |
| 285 | |
| 286 | // recoverSweeper is the panic firewall for the long-running background |
| 287 | // sweeper loops (orphan GC, op-log GC, token reaper, workspace purge). |