58 lines
1.4 KiB
Go
58 lines
1.4 KiB
Go
// Package lifecycle holds helpers shared by the components that
|
|
// register fx start and stop hooks.
|
|
package lifecycle
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"sync"
|
|
)
|
|
|
|
// WaitForShutdown waits for wg to drain, bounded by ctx.
|
|
//
|
|
// fx hands OnStop a context carrying the application's stop
|
|
// timeout. A bare wg.Wait() discards that deadline, so a single
|
|
// goroutine that never observes cancellation — a delivery target
|
|
// that never returns, a SQLite operation blocked on a lock —
|
|
// hangs the process forever instead of letting it exit when the
|
|
// timeout expires, which is exactly when a clean shutdown matters
|
|
// most.
|
|
//
|
|
// On timeout it logs at error naming component and returns an
|
|
// error: the goroutines are still running, and reporting success
|
|
// would hide an unclean shutdown from the operator. The waiting
|
|
// goroutine outlives this call and exits when (if) wg drains; it
|
|
// holds nothing but the channel it closes.
|
|
func WaitForShutdown(
|
|
ctx context.Context,
|
|
log *slog.Logger,
|
|
component string,
|
|
wg *sync.WaitGroup,
|
|
) error {
|
|
done := make(chan struct{})
|
|
|
|
go func() {
|
|
defer close(done)
|
|
|
|
wg.Wait()
|
|
}()
|
|
|
|
select {
|
|
case <-done:
|
|
return nil
|
|
case <-ctx.Done():
|
|
log.Error(
|
|
"shutdown timed out, goroutines still running",
|
|
"component", component,
|
|
"error", ctx.Err(),
|
|
)
|
|
|
|
return fmt.Errorf(
|
|
"%s: shutdown timed out, "+
|
|
"goroutines still running: %w",
|
|
component, ctx.Err(),
|
|
)
|
|
}
|
|
}
|