fire

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 22, 2026 License: MIT Imports: 2 Imported by: 0

README ¶

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:

  1. starts all components concurrently
  2. waits for either context cancellation or a component failure
  3. gracefully stops all registered components in reverse order
  4. 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,
)

Documentation ¶

Index ¶

Constants ¶

This section is empty.

Variables ¶

This section is empty.

Functions ¶

func WithContext ¶

func WithContext(f func() error) func(context.Context) error

Types ¶

type Group ¶

type Group struct {
	Cancel context.CancelFunc
	// contains filtered or unexported fields
}

func New ¶

func New(ctx context.Context) (*Group, context.Context)

func (*Group) Add ¶

func (g *Group) Add(run, stop function, onRunErr, onStopErr func(error))

func (*Group) Wait ¶

func (g *Group) Wait()

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL