Coda is a Go library that helps manage graceful shutdowns of concurrent applications. It provides a structured way to organize goroutines into groups with dependencies and ensures they shut down in the correct order.
- Group-based goroutine management
- Dependency-based shutdown ordering
- Configurable timeouts and behavior
- Flexible logging options
- Error handling and propagation
go get github.com/marnixbouhuis/coda
shutdown := coda.NewShutdown()
// Create groups
dbGroup := coda.Must(shutdown.NewGroup("database", nil))
workerGroup := coda.Must(shutdown.NewGroup("worker", []*coda.Group{dbGroup}))
// Add goroutines to groups
dbGroup.Go(func(ctx context.Context, ready func()) error {
// Do startup work here
ready()
// Wait for shutdown signal
<-ctx.Done()
// Do tear down logic here
return nil
})
// Wait for shutdown
err := shutdown.Wait()
The shutdown manager is the main coordinator that manages groups and their shutdown sequence. Create one using NewShutdown()
.
Options:
WithShutdownLogger(logger Logger)
- Set a custom logger (default: NoopLogger)
Groups are collections of goroutines that should be shut down together. Groups can depend on other groups, creating a shutdown hierarchy.
Create groups using shutdown.NewGroup(name string, dependencies []*Group, opts ...GroupOption)
.
Group options:
WithGroupShutdownTimeout(duration)
- Maximum time to wait for group shutdown (default: no timeout)
Add goroutines to groups using the Go()
method. Each goroutine receives a context and a ready function.
group.Go(func(ctx context.Context, ready func()) error {
// Do startup here
// Signal readiness
ready()
// Wait for shutdown
<-ctx.Done()
// Do shutdown here
return nil
}, opts...)
Goroutine options:
WithReadyTimeout(duration)
- Maximum time to wait for ready signal (default: no timeout)WithCrashOnReadyTimeoutHit(bool)
- Stop everything if ready timeout is hit (default: true)WithCrashOnError(bool)
- Stop everything if goroutine returns error (default: true)WithCrashOnEarlyStop(bool)
- Stop everything if goroutine exits before shutdown (default: true)WithBlock(bool)
- Block until goroutine signals ready (default: false)
func Example() {
sd := coda.NewShutdown(
coda.WithShutdownLogger(coda.NewStdLogger(log.Default())),
)
// Create groups with dependencies
dbGroup := coda.Must(sd.NewGroup("database", nil,
coda.WithGroupShutdownTimeout(5*time.Second),
))
workerGroup := coda.Must(sd.NewGroup("workers", []*coda.Group{dbGroup},
coda.WithGroupShutdownTimeout(10*time.Second),
))
// Database connection
dbGroup.Go(func(ctx context.Context, ready func()) error {
db, err := sql.Open("postgres", "connection-string")
if err != nil {
return err
}
defer db.Close()
ready()
<-ctx.Done()
return nil
}, coda.WithBlock(true))
// Start multiple workers
for workerID := range 3 {
workerGroup.Go(func(ctx context.Context, ready func()) error {
log.Printf("Worker %d starting", workerID)
ready()
for {
select {
case <-ctx.Done():
log.Printf("Worker %d shutting down", workerID)
return nil
case <-time.After(time.Second):
// Do some work
log.Printf("Worker %d processing", workerID)
}
}
}, coda.WithBlock(true))
}
serverGroup := coda.Must(sd.NewGroup("server", []*coda.Group{workerGroup, dbGroup}))
serverGroup.Go(func(ctx context.Context, ready func()) error {
ready()
mux := http.NewServeMux()
srv := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
}
mux.HandleFunc("/demo", func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), time.Second*30)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("Failed to stop HTTP server gracefully: %v", err)
sd.StopWithError(err)
}
}()
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
})
go func() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
<-ch
sd.Stop()
}()
if err := sd.Wait(); err != nil {
log.Printf("Shutdown error: %v", err)
os.Exit(1)
}
}
Errors can occur in several ways:
- Goroutine returns an error
- Ready timeout is hit
- Goroutine stops early
- Shutdown timeout is hit
Configure how these errors are handled using the appropriate options. Errors can either crash the entire shutdown process or be logged and ignored.
Coda supports custom logging through the Logger
interface:
type Logger interface {
Info(str string)
Error(str string)
}
Built-in loggers:
NewNoopLogger()
- Discards all logs (default)NewStdLogger(logger *log.Logger)
- Adapts standard library logger
Loggers available as external module:
This project is licensed under the MIT License - see the LICENSE.md file for details.