go-srvc / srvc

github.com/go-srvc/srvc

pkg.go.dev source
go get github.com/go-srvc/srvc@v0.3.0

README

Go Reference codecov main

Simple, Safe, and Modular Service Runner

srvc library provides a simple but powerful interface with zero external dependencies for running service modules.

Use Case

Normally Go services are composed of multiple "modules" which each run in their own goroutine such as http server, signal listener, kafka consumer, ticker, etc. These modules should remain alive throughout the lifecycle of the whole service, and if one goes down, graceful exit should be executed to avoid "zombie" services. srvc takes care of all this via a simple module interface.

List of ready made modules can be found under github.com/go-srvc/mods

Usage

Main package

package main

import (
	"fmt"
	"net/http"

	"github.com/go-srvc/mods/httpmod"
	"github.com/go-srvc/mods/logmod"
	"github.com/go-srvc/mods/metermod"
	"github.com/go-srvc/mods/sigmod"
	"github.com/go-srvc/mods/sqlxmod"
	"github.com/go-srvc/mods/tracemod"
	"github.com/go-srvc/srvc"
)

func main() {
	db := sqlxmod.New()
	srvc.RunAndExit(
		logmod.New(),
		sigmod.New(),
		tracemod.New(),
		metermod.New(),
		db,
		httpmod.New(
			httpmod.WithAddr(":8080"),
			httpmod.WithHandler(handler(db)),
		),
	)
}

func handler(db *sqlxmod.DB) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if err := db.DB().PingContext(r.Context()); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		fmt.Fprint(w, "OK")
	})
}

Implementing custom modules

package main

import "github.com/go-srvc/srvc"

func main() {
	srvc.RunAndExit(&MyMod{})
}

type MyMod struct {
	done chan struct{}
}

func (m *MyMod) Init() error {
	m.done = make(chan struct{})
	return nil
}

// Run should block until the module is stopped.
// If you don't have a blocking operation, you can use done channel to block.
func (m *MyMod) Run() error {
	<-m.done
	return nil
}

func (m *MyMod) Stop() error {
	defer close(m.done)
	return nil
}

func (m *MyMod) ID() string { return "MyMod" }

Lifecycle

Run executes modules through a deterministic lifecycle:

  1. Init is called sequentially in the order modules are passed. If any Init returns an error, the loop stops and Stop is called on already-initialized modules in reverse order. Uninitialized modules never get Init or Stop.
  2. Run is started for each successfully initialized module in its own goroutine. Start order is not guaranteed.
  3. When the first Run returns (with or without error), the service moves to shutdown.
  4. Stop is called sequentially in reverse order on every initialized module. Each module's Stop must cause its Run to return.
  5. Run blocks until every Run goroutine has returned, then returns the joined errors from Init, Run, and Stop (or nil).

Panic recovery

Panics inside Init, Run, or Stop are recovered. The stack trace is logged, and the panic is converted into an error wrapping srvc.ErrModulePanic so other modules can still shut down gracefully.

Exit behaviour

RunAndExit calls os.Exit(1) if Run returns any error, and returns normally on success.

Contracts modules must uphold

  • Stop must make Run return. If Run ignores Stop, srvc.Run will block in its final wait. There is no built-in shutdown timeout, so a stuck module hangs the service.
  • ID should return a stable, unique identifier used for log attribution.

Acknowledgements

This library is something I have found myself writing over and over again in every project I been part of. One of the iterations can be found under https://github.com/elisasre/go-common.

Overview

Package srvc provides simple but powerful Run functionality on top of Module abstraction. Ready made modules can be found under: github.com/go-srvc/mods.

Constants

const ErrModulePanic = ErrStr("module recovered from panic")

Variables

var JoinErrors = errors.Join

JoinErrors is used by srvc to combine multiple errors into one. Override it to plug in custom multi-error formatting.

Functions

func Run

func Run(modules ...Module) error

Run will run all given modules using following control flow:

  1. Exec Init() for each module in order. If any Init() returns error the Init loop is stopped and Stop() will be called for already initialized modules in reverse order.
  2. Exec Run() for each module in own goroutine so order isn't guaranteed.
  3. Wait for any Run() function to return nil or an error and move to Stop loop.
  4. Exec Stop() for modules in reverse order.
  5. Wait for all Run() goroutines to return.
  6. Return all errors or nil

Panics inside modules are recovered so other modules can shut down gracefully. Recovered panics are returned as errors wrapping ErrModulePanic.

func RunAndExit

func RunAndExit(modules ...Module)

RunAndExit is convenience wrapper for Run that calls os.Exit with code 1 in case of an error. The common use case is to srvc.RunAndExit from main function and let the srvc handle the rest.

package main

import "github.com/go-srvc/srvc"

func main() {
	srvc.RunAndExit(
	// Add your modules here
	)
}

Types

type ErrGroup

type ErrGroup struct {
	// contains filtered or unexported fields
}

ErrGroup is a goroutine group that waits for all goroutines to finish and collects errors.

func (*ErrGroup) Go

func (eg *ErrGroup) Go(f func() error)

Go runs the given function in a goroutine.

func (*ErrGroup) Wait

func (eg *ErrGroup) Wait() error

Wait waits for all goroutines to finish and returns all errors that occurred.

type ErrStr

type ErrStr string

ErrStr adds Error method to string type.

func (ErrStr) Error

func (e ErrStr) Error() string

type Module

type Module interface {
	// ID should return identifier for logging purposes.
	ID() string
	// Init allows synchronous initialization of module.
	Init() error
	// Run should start the module and block until stop is called or error occurs.
	Run() error
	// Stop is called for synchronous cleanup and must cause Run to return.
	// If Init ran, Stop is guaranteed to run as part of cleanup.
	Stop() error
}

Examples

ExampleRun

package main

import (
	"fmt"

	"github.com/go-srvc/srvc"
)

type printMod struct{}

func (m *printMod) ID() string  { return "printMod" }
func (m *printMod) Init() error { return nil }
func (m *printMod) Run() error  { fmt.Println("hello"); return nil }
func (m *printMod) Stop() error { return nil }

func main() {
	_ = srvc.Run(&printMod{})
}