goservices
is a Go package to help manage services.
For now, it is notably used in Gluetun and qdm12/dns to run multiple servers and 'loops' in the same program.
🚧 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)
}
- the code is fully test covered
- Zero dependency (except for tests with
golang/mock
andstretchr/testify
) - - 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.
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
}
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
}
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
}
You can implement yourself the interface. A good thread safe example to follow would be the httpserver service implementation. You can also refer to the service implementation section in the design document.
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 previous implementation of the httpserver
service which was using this RunWrapper
.
This library provides a few pre-built services: