About
fire is a small library for coordinating independent long-running components such as HTTP servers, gRPC servers, workers, consumers or schedulers.
Unlike errgroup and oklog/run, fire does not treat every component failure as a fatal group failure. Each component has its own error callbacks, allowing the application to decide whether a failure should trigger shutdown or be handled independently.
Comparison
errgroup
errgroup follows a fail-fast model: an error from any goroutine cancels the group context and stops all other goroutines.
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
defer cancel()
server1 := NewServer("0.0.0.0:1111")
server2 := NewServer("0.0.0.0:2222")
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error {
if err := server1.Run(); err != nil {
return fmt.Errorf("server-1 failed: %w", err)
}
return nil
})
g.Go(func() error {
if err := server2.Run(); err != nil {
return fmt.Errorf("server-2 failed: %w", err)
}
return nil
})
<-ctx.Done()
zap.L().Info("Context cancelled. Initiating graceful shutdown for all servers...")
if err := server2.Stop(); err != nil {
zap.L().Warn("failed to stop server-2 gracefully", zap.Error(err))
}
if err := server1.Stop(); err != nil {
zap.L().Warn("failed to stop server-1 gracefully", zap.Error(err))
}
if err := g.Wait(); err != nil {
zap.L().Error("Application stopped with runtime error", zap.Error(err))
os.Exit(1)
}
oklog/run
oklog/run uses an actor model where termination of one actor interrupts all other actors. It is great for coordinated shutdown, but does not provide per-component failure policies.
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
defer cancel()
server1 := NewServer("0.0.0.0:1111")
server2 := NewServer("0.0.0.0:2222")
var g run.Group
g.Add(
func() error {
return server1.Run()
},
func(err error) {
if err != nil {
zap.L().Error("failed to run server-1", zap.Error(err))
}
if err = server1.Stop(); err != nil {
zap.L().Warn("failed to stop server-1", zap.Error(err))
}
},
)
g.Add(
func() error {
return server2.Run()
},
func(err error) {
if err != nil {
zap.L().Error("failed to run server-2", zap.Error(err))
}
if err = server2.Stop(); err != nil {
zap.L().Warn("failed to stop server-2", zap.Error(err))
}
},
)
g.Add(
func() error {
<-ctx.Done()
return nil
},
func(error) {
cancel()
},
)
_ = g.Run()
🔥 fire
fire separates lifecycle management from error policy: components have their own Run, Stop, onRunErr and onStopErr handlers, allowing different shutdown behavior for different components.
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
defer cancel()
server1 := NewServer("0.0.0.0:1111")
server2 := NewServer("0.0.0.0:2222")
g, ctx := fire.New(ctx)
g.Add(
fire.WithContext(server1.Run),
server1.Stop,
func(err error) {
zap.L().Error("failed to run server-1", zap.Error(err))
g.Cancel()
},
func(err error) {
zap.L().Warn("failed to stop server-1", zap.Error(err))
},
)
g.Add(
fire.WithContext(server2.Run),
server2.Stop,
func(err error) {
zap.L().Error("failed to run server-2", zap.Error(err))
// do NOT cancel intentionally
},
func(err error) {
zap.L().Error("failed to stop server-2", zap.Error(err))
},
)
g.Wait()
Why?
Managing multiple long-running components usually requires:
- starting goroutines;
- waiting for signals or failures;
- gracefully stopping components;
- handling different error policies.
fire keeps this logic in one place while leaving error decisions to the application.
How it works
fire manages the lifecycle of independent long-running components.
Register each component with a Run function, a Stop function, and optional error callbacks.
fire:
- starts all components concurrently
- waits for either context cancellation or a component failure
- gracefully stops all registered components in reverse order
- waits for their
Run functions to finish.
Run errors are handled by the component's callback, which decides whether the application should shut down or continue running.
Stop errors are handled separately and do not affect other components.
For existing APIs without context support, use fire.WithContext():
g.Add(
fire.WithContext(server.Run),
fire.WithContext(server.Stop),
onRunError,
onStopError,
)
For context-aware components, pass functions directly:
g.Add(
server.Run,
server.Stop,
onRunError,
onStopError,
)