goservices

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 15, 2023 License: MIT Imports: 6 Imported by: 6

README

Go services

goservices is a Go package to help manage services.

🚧 Logo to be added 🚧

What is a service? It's this interface currently:

type Service interface {
 // String returns the service name.
 // It is assumed to be constant over the lifetime of the service.
 String() string
 // Start starts the service.
 // On success, it returns a run error channel and a nil error.
 // On failure, it returns a nil run error channel and an error.
 // If the service crashes, only one single error should be sent in
 // the error channel.
 // When the service is stopped, the service should NOT send an error
 // in the run error channel or close this one.
 // Start takes in a context and the implementation should promptly return
 // the context error wrapped in `startErr` if the context is canceled.
 Start(ctx context.Context) (runError <-chan error, startErr error)
 // Stops stops the service.
 // A service should NOT close or write an error to its run error channel
 // if it is stopped.
 Stop() (err error)
}

Stability

  • the code is fully test covered
  • Zero dependency (except for tests with golang/mock and stretchr/testify) - gographs
  • the Go API should be stable until a v1.0.0 release
  • the Go API will be guaranteed stable from the v1.0.0 release
  • the code is linted with golangci-lint and a lot of linters
  • There is a CI pipeline to test, lint, check mocks and check documentation on every commit.

Sequence of services

To start and stop a sequence of services, you can use the Sequence type. Note it itself implements the Service interface, so you can nest it with other service management types, like Group.

 ctx := context.Background()

 settings := goservices.SequenceSettings{
  ServicesStart: []goservices.Service{serviceA, serviceB},
  ServicesStop:  []goservices.Service{serviceB, serviceA},
 }
 sequence, err := goservices.NewSequence(settings)
 if err != nil {
  return fmt.Errorf("creating services sequence: %w", err)
 }


 runError, err := sequence.Start(ctx)
 if err != nil {
  return fmt.Errorf("starting services sequence: %w", err)
 }

 select {
 case err = <-runError:
  return fmt.Errorf("services sequence crashed: %w", err)
 case <-ctx.Done():
  err = sequence.Stop()
  if err != nil {
   return fmt.Errorf("stopping services sequence: %w", err)
  }
  return nil
 }

🏃 runnable example

Group of services

To start and stop a group of services all in parallel, you can use the Group type. Note it itself implements the Service interface, so you can nest it with other service management types, like Sequence.

A simplistic example would be:

 ctx := context.Background()

 settings := goservices.GroupSettings{
  Services: []goservices.Service{serviceA, serviceB},
 }
 group, err := goservices.NewGroup(settings)
 if err != nil {
  return fmt.Errorf("creating services group: %w", err)
 }

 runError, err := group.Start(ctx)
 if err != nil {
  return fmt.Errorf("starting services group: %w", err)
 }

 select {
 case err = <-runError:
  return fmt.Errorf("services group crashed: %w", err)
 case <-ctx.Done():
  err = group.Stop()
  if err != nil {
   return fmt.Errorf("stopping services group: %w", err)
  }
  return nil
 }

🏃 runnable example

Auto-restart a service

To automatically restart a service when it crashes, you can use the Restarter type. Note it itself implements the Service interface, so you can nest it with other service management types, like Sequence.

 ctx := context.Background()

 settings := goservices.RestarterSettings{
  Service: serviceToRestart,
 }
 restarter, err := goservices.NewRestarter(settings)
 if err != nil {
  return fmt.Errorf("creating restarter: %w", err)
 }

 runError, startErr := restarter.Start(ctx)
 if startErr != nil {
  return fmt.Errorf("starting restarter: %w", startErr)
 }

 select {
 case err = <-runError:
  return fmt.Errorf("restarter crashed: %w", err)
 case <-ctx.Done():
  err = restarter.Stop()
  if err != nil {
   return fmt.Errorf("stopping restarter: %w", err)
  }
  return nil
 }

🏃 runnable example

Create a service

You can implement yourself the interface.

HOWEVER this is tedious to get right especially with the many race conditions possible (i.e. what if the service crashes at the same time as it is stopped?).

This is why this library provides a RunWrapper which creates a service from a RunFunction:

type RunFunction func(ctx context.Context,
 ready chan<- struct{}, runError, stopError chan<- error)

Please see the documentation of the RunFunction to know the details on how to implement it correctly.

A concrete example is the httpserver service which is implemented using this RunWrapper.

Pre-built services

This library provides a few pre-built services:

Main branch dependency graph

gographs

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrServiceIsNil = errors.New("service is nil")

	ErrNoService = errors.New("no service specified")

	ErrNoServiceStart            = errors.New("no service start order specified")
	ErrNoServiceStop             = errors.New("no service stop order specified")
	ErrServicesStartStopMismatch = errors.New("services to start and stop mismatch")
	ErrServicesNotUnique         = errors.New("services are not unique")

	ErrAlreadyStarted = errors.New("already started")
	ErrAlreadyStopped = errors.New("already stopped")
)

Functions

This section is empty.

Types

type Group

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

Group is a group of services to start and stop in parallel. It implements the Service interface itself.

func NewGroup

func NewGroup(settings GroupSettings) (group *Group, err error)

NewGroup creates a new group of services given the settings, and returns an error if any setting is not valid.

func (*Group) Start

func (g *Group) Start(ctx context.Context) (runError <-chan error, startErr error)

Start starts services specified in parallel.

If a service fails to start, the `startErr` is returned and all other running services are stopped.

If a service fails after `Start` returns without error, all other running services are stopped and the error is sent in the `runError` channel which is then closed. A caller should listen on `runError` until the `Stop` method call fully completes, since a run error can theoretically happen at the same time the caller calls `Stop` on the group.

If the group is already running, the `ErrAlreadyStarted` error is returned.

If the context is canceled, all the starting operations are canceled, all already running services are stopped and the context error is wrapped in the `startErr` returned.

func (*Group) Stop

func (g *Group) Stop() (err error)

Stop stops running services of the group in parallel. If an error occurs for any of the service stop, the other running services will still be stopped. Only the first non nil service stop error encountered is returned, but the hooks can be used to process each error returned. If the group is already stopped, the `ErrAlreadyStopped` error is returned.

func (*Group) String

func (g *Group) String() string

type GroupSettings

type GroupSettings struct {
	// Name is the sequence name, used for hooks and errors.
	Name string
	// Services specifies the services to start and stop in parallel.
	// Note their order does not matter.
	Services []Service
	// Hooks are hooks to call when starting and stopping
	// each service. Hooks method calls should be thread safe
	// since its methods are called in parallel goroutines.
	// It defaults to a no-op hooks implementation if left unset.
	Hooks Hooks
}

GroupSettings contains settings for a group of services.

type Hooks

type Hooks interface {
	HooksStart
	HooksStop
	HooksCrash
}

HooksStart is the interface required to hook into service events.

type HooksCrash

type HooksCrash interface {
	OnCrash(service string, err error)
}

HooksCrash is the interface required to hook into service crash events.

type HooksStart

type HooksStart interface {
	OnStart(service string)
	OnStarted(service string, err error)
}

HooksStart is the interface required to hook into service start and started events.

type HooksStop

type HooksStop interface {
	OnStop(service string)
	OnStopped(service string, err error)
}

HooksStop is the interface required to hook into service stop and stopped events.

type Restarter

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

Restarter implements a service which restarts an underlying service if it crashes. The restarter only crashes if the underlying services fails to start on a subsequent run.

func NewRestarter

func NewRestarter(settings RestarterSettings) (restarter *Restarter, err error)

NewRestarter creates a new restarter given the settings. It returns an error if any of the settings is not valid.

func (*Restarter) Start

func (r *Restarter) Start(ctx context.Context) (runError <-chan error, startErr error)

Start starts the underlying service.

If the underlying service fails to start, the `startErr` is returned.

If the underlying service fails after this method call returns without error, it is automatically restarted and no error is emitted in the `runError` channel.

If a subsequent service start fails, the start error is sent in the `runError` channel, this channel is closed and the restarter stops. A caller should listen on `runError` until the `Stop` method call fully completes, since a run error can theoretically happen at the same time the caller calls `Stop` on the restarter.

If the restarter is already running, the `ErrAlreadyStarted` error is returned.

If the context is canceled, the service starting operation is canceled, and the context error is wrapped in the `startErr` returned.

func (*Restarter) Stop

func (r *Restarter) Stop() (err error)

Stop stops the underlying service and the internal run error restart-watcher goroutine. If the restarter is already stopped, the `ErrAlreadyStopped` error is returned. Note if the restarter is currently restarting the underlying service, it has to finish the start before the stopping can start.

func (*Restarter) String

func (r *Restarter) String() string

type RestarterSettings

type RestarterSettings struct {
	// Service is the service to restart.
	// It must be set for settings validation to succeed.
	Service Service
	// Hooks are hooks to call when the service starts,
	// stops or crashes. It defaults to a noop hooks
	// implementation.
	Hooks Hooks
}

RestarterSettings contains settings for a restarter.

type RunFunction

type RunFunction func(ctx context.Context,
	ready chan<- struct{}, runError, stopError chan<- error)

RunFunction is a functional type to simplify a service implementation together with `NewRunWrapper`.

  • `ctx` must be listened on to trigger a stop. Note the `stopError` must be written to when stopping.
  • `ready` must be closed as soon as the run function has started successfully. Often a simple `close(ready)` at the start of the run body code is enough.
  • `runError` must have a non-nil error written to it if the run function fails unexpectedly, and the function must promptly `return` right after writing the error. If the function is stopped, the `runError` channel should not be written to nor closed. Alternatively, if a run error is written to this channel, the `stopError` channel should not be written to nor closed. If an error occurs before the run function is ready, the `ready` channel must not be closed. The `runError` channel should be closed after writing an error to it to prevent further writes. Note if an error is written to this channel, the run wrapper service will be considered as crashed.
  • `stopError` must have an error written to it when the context gets canceled, to signal the stopping result. In the case of no stopping error, the channel must be closed. Otherwise, the error must be written to the channel and the channel should then be closed to prevent further writes. The run function must `return` right after.

A very simple run function template would be:

func run(ctx context.Context, ready chan<- struct{},
	runError, stopError chan<- error) {
	close(ready)
	select {
	case <-ctx.Done():
		// cleanup
		close(stopError) // successful stop
		return
	case err := <-someChannel:
		if err != nil {
			runError <- err
			close(runError)
			return
		}
	}
}

type RunWrapper

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

RunWrapper is a service implementation taking care of the many edge cases and race conditions that can occur when running a service, and uses a user-injected `RunFunction` to run the service.

func NewRunWrapper

func NewRunWrapper(name string, run RunFunction) *RunWrapper

NewRunWrapper creates a new service wrapper using the service name and run function given.

func (*RunWrapper) Start

func (w *RunWrapper) Start(startCtx context.Context) (runError <-chan error, startErr error)

Start starts the service and is thread safe. It returns a `runError` channel which the caller should listen on to catch an eventual run error from the underlying run function, as well as a `startErr` error which can be non-nil if the service failed to start. Start takes in a context which is monitored for until the run function signals it is ready by closing its ready channel.

func (*RunWrapper) Stop

func (w *RunWrapper) Stop() (err error)

Stop stops the service and is thread safe. It returns a non-nil error in the following cases:

  • the underlying run function failed to stop and wrote an error to its `stopError` channel
  • the service is already stopped
  • the service is already crashed

func (*RunWrapper) String

func (w *RunWrapper) String() string

String returns the name of the service.

type Sequence

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

Sequence is a sequence of services to start and stop in a pre-defined order. It implements the Service interface itself.

func NewSequence

func NewSequence(settings SequenceSettings) (sequence *Sequence, err error)

NewSequence creates a new sequence of services given the settings, and returns an error if any setting is not valid.

func (*Sequence) Start

func (s *Sequence) Start(ctx context.Context) (runError <-chan error, startErr error)

Start starts services in the order specified by the sequence of services.

If a service fails to start, the `startErr` is returned and all other running services are stopped in the order specified by the stop sequence of services.

If a service fails after this method call returns without error, all other running services are stopped and the error is returned in the `runError` channel which is then closed. A caller should listen on `runError` until the `Stop` method call fully completes, since a run error can theoretically happen at the same time the caller calls `Stop` on the sequence.

If the sequence is already running then a start error ErrAlreadyStarted is returned.

If the context is canceled, the current service starting is canceled, all already running services are stopped and the context error is wrapped in the `startErr` returned.

func (*Sequence) Stop

func (s *Sequence) Stop() (err error)

Stop stops running services of the sequence in the order specified by the sequence of services. If an error occurs for any of the service stop, the other running services will still be stopped. Only the first non nil service stop error encountered is returned, but the hooks can be used to process each error returned. If the sequence is already stopped, the `ErrAlreadyStopped` error is returned.

func (*Sequence) String

func (s *Sequence) String() string

type SequenceSettings

type SequenceSettings struct {
	// Name is the sequence name, used for hooks and errors.
	Name string
	// ServicesStart specifies an order of services
	// to start and must be set.
	ServicesStart []Service
	// ServicesStart specifies an order of services
	// to stop and must be set.
	ServicesStop []Service
	// Hooks are hooks to call when starting and stopping
	// each service. It defaults to a noop hooks
	// implementation.
	Hooks Hooks
}

SequenceSettings contains settings for a sequence of services.

type Service

type Service interface {
	Starter
	Stopper
	// String returns the service name.
	// It is assumed to be constant over the lifetime of the service.
	String() string
}

Service is the interface for a service that can be started, stopped and stringed.

type Starter

type Starter interface {
	// String returns the starter name.
	// It is assumed to be constant over the lifetime of the starter.
	String() string
	// Start starts the service.
	// On success, it returns a run error channel and a nil error.
	// On failure, it returns a nil run error channel and an error.
	// If the service crashes, only one single error should be sent in
	// the error channel.
	// When the service is stopped, the service should NOT send an error
	// in the run error channel or close this one.
	// Start takes in a context and the implementation should promptly return
	// the context error wrapped in `startErr` if the context is canceled.
	Start(ctx context.Context) (runError <-chan error, startErr error)
}

Starter is the interface for a service that can be started.

type State

type State uint8

State is the state of a service. Is it exported to ease the implementation of services.

const (
	// StateStopped is the state of a service that is stopped.
	StateStopped State = iota
	// StateStarting is the state of a service that is starting.
	StateStarting
	// StateRunning is the state of a service that is running.
	StateRunning
	// StateStopping is the state of a service that is stopping.
	StateStopping
	// StateCrashed is the state of a service that has crashed.
	StateCrashed
)

func (State) String

func (s State) String() string

type Stopper

type Stopper interface {
	// String returns the stopper name.
	// It is assumed to be constant over the lifetime of the stopper.
	String() string
	// Stops stops the service.
	// A service should NOT close or write an error to its run error channel
	// if it is stopped.
	Stop() (err error)
}

Stopper is the interface for a service that can be stopped.

Directories

Path Synopsis
examples
group command
restarter command
sequence command
Package httpserver implements an HTTP server.
Package httpserver implements an HTTP server.

Jump to

Keyboard shortcuts

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