Categorygithub.com/containerssh/service
modulepackage
1.0.0
Repository: https://github.com/containerssh/service.git
Documentation: pkg.go.dev

# README

ContainerSSH - Launch Containers on Demand

ContainerSSH Service Library

Go Report Card LGTM Alerts

This library provides a common way to manage multiple independent services in a single binary.

⚠⚠⚠ Warning: This is a developer documentation. ⚠⚠⚠
The user documentation for ContainerSSH is located at containerssh.io.

Creating a service

In order to create a service you must implement the Service interface:

type Service interface {
	// String returns the name of the service
	String() string

	RunWithLifecycle(lifecycle Lifecycle) error
}

The Run function gets passed a Lifecycle object. It must call the appropriate lifecycle hooks:

func (s *myService) RunWithLifecycle(lifecycle Lifecycle) error {
    //Do initialization here
    lifecycle.Running()
    for {
        // Do something
        if err != nil {
            return err
        }
        if lifecycle.ShouldStop() {
            shutdownContext := lifecycle.Stopping()
            // Handle graceful shutdown.
            // If shutdownContext expires, shut down immediately.
            // Then exit out of the loop.
            break
        }
    }
    return nil
}

For advanced use cases you can replace the lifecycle.ShouldStop() call with fetching the context directly using lifecycle.Context(). You can then use the context in a select statement.

Warning! Do not call RunWithLifecycle() on the service directly. Instead, always call Run() on the lifecycle to enable accurate state tracking and error handling.

Creating a lifecycle

In order to run a service you need to create a Lifecycle object. Since Lifecycle is an interface you can implement it yourself, or you can use the default implementation:

lifecycle := service.NewLifecycle(service)

The service parameter should be the associated service. The lifecycle can be used to add hooks to the service. Calling these functions multiple times is supported, but the call order of hook functions is not guaranteed.

lifecycle.OnStateChange(func(s service.Service, l service.Lifecycle, newState service.State) {
    // do something
})
lifecycle.OnStarting(func(s service.Service, l service.Lifecycle) {
    // do something
})
lifecycle.OnRunning(func(s service.Service, l service.Lifecycle) {
    // do something
})
lifecycle.OnStopping(func(s service.Service, l service.Lifecycle, shutdownContext context.Context) {
    // do something
})
lifecycle.OnStopped(func(s service.Service, l service.Lifecycle) {
    // do something
})
lifecycle.OnCrashed(func(s service.Service, l service.Lifecycle, err error) {
    // do something
})

These hook functions can also be chained:

lifecycle.OnStarting(myHandler).OnRunning(myHandler)

You can now use the Lifecycle to run the service:

err := lifecycle.Run()

Warning! Do not call RunWithLifecycle() on the service directly. Instead, always call Run() on the lifecycle to enable accurate state tracking and error handling.

Using the service pool

One of the advanced components in this library is the Pool object. It provides an overlay for managing multiple services in parallel, and it implements the Service interface itself. In other words, it can be nested.

First, let's create a pool:

pool := service.NewPool(
    service.NewLifecycleFactory(),
    logger,
)

The logger variable is a logger from the log package. You can then add subservices to the pool. When adding a service the pool will return the lifecycle object you can use to add hooks. The hook functions can be chained for easier configuration:

_ = pool.
    Add(myService1).
    OnRunning(func (s Service, l Lifecycle) {
        log.Printf("%s is now %s", s.String(), l.State())
    })

Once the services are added the pool can be launched:

lifecycle := service.NewLifecycle(pool)
go func() {
    err := lifecycle.Run()
    // Handle errors here
}
lifecycle.Shutdown(context.Background())

Ideally, the pool can be used to handle Ctrl+C and SIGTERM events:

signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
go func() {
    if _, ok := <-signals; ok {
        // ok means the channel wasn't closed
        lifecycle.Shutdown(
            context.WithTimeout(
                context.Background(),
                20 * time.Second,
            )
        )
    }
}()
// Wait for the pool to terminate.
lifecycle.Wait()
// We are already shutting down, ignore further signals
signal.Ignore(syscall.SIGINT, syscall.SIGTERM)
// close signals channel so the signal handler gets terminated
close(signals)

# Functions

NewLifecycle creates a new lifecycle for the specified service.
NewLifecycleFactory creates a new default factory for lifecycles.
NewPool creates a new service pool that can be used to run and manage multiple services in parallel.

# Constants

A ContainerSSH has stopped improperly.
A ContainerSSH service is now running.
All ContainerSSH services are now running.
All ContainerSSH services are starting.
ContainerSSH has stopped all services.
ContainerSSH is stopping all services.
ContainerSSH is starting a component service.
A ContainerSSH service has stopped.
A ContainerSSH service is now stopping.
StateCrashed means that the service has exited with an error.
StateRunning means that the service is running normally and is serving requests.
StateStarting means that the service is running currently initializing.
StateStopped means that the service is currently not running.
StateStopping means that the service has received a stop signal and is currently stopping gracefully.

# Structs

AbstractService is a struct that can be inherited to impl.

# Interfaces

Lifecycle contains hooks for the Service the Run functions needs to call as it enters each lifecycle stage.
LifecycleFactory is an interface to create lifecycle objects in pools.
Pool is a handler for multiple services at once.
Service is an interface that specifies the minimum requirements for a generic service.

# Type aliases

State describes the current state the service is in.