package adapter import ( "runtime/debug" "time" ) // RecoverAndLog recovers a panic (if any) and logs it with a stack trace. // Call it via `defer adapter.RecoverAndLog(logger, "name")` at the top of // any goroutine body that must never be allowed to crash the process. func RecoverAndLog(logger Logger, name string) { if r := recover(); r != nil { logger.Error("recovered from panic", "component", name, "panic", r, "stack", string(debug.Stack())) } } // SafeGo runs fn in a new goroutine, recovering any panic so it can never // crash the process. Use for one-shot/fire-and-forget goroutines; a panic is // logged (with its stack trace) and the goroutine simply ends. func SafeGo(logger Logger, name string, fn func()) { go func() { defer RecoverAndLog(logger, name) fn() }() } // SupervisedGo runs fn in a new goroutine. If fn panics, the panic is logged // and fn is restarted (after a short backoff) instead of letting the // goroutine die permanently. Use for long-running loops (ticker routines, // notification listeners, worker loops) that must keep running for the life // of the process. fn must return normally, without panicking, once its own // shutdown/context-done condition is met -- a clean return is not restarted. func SupervisedGo(logger Logger, name string, fn func()) { go func() { for { if runSupervised(logger, name, fn) { return } time.Sleep(time.Second) } }() } // runSupervised runs fn once, recovering a panic if it occurs. It returns // true if fn returned normally (no restart needed) and false if it panicked // (caller should restart it). func runSupervised(logger Logger, name string, fn func()) (clean bool) { defer func() { if r := recover(); r != nil { logger.Error("recovered from panic, restarting", "component", name, "panic", r, "stack", string(debug.Stack())) clean = false } }() fn() return true }