Skip to content
This repository has been archived by the owner on Oct 2, 2022. It is now read-only.

Releases: ContainerSSH/service

1.0.0: First stable version

01 Apr 13:46
Compare
Choose a tag to compare

This release tags the first stable version for ContainerSSH 0.4.0.

0.9.3: Version bump, codes doc

08 Mar 20:56
Compare
Choose a tag to compare
Pre-release

This release adds the CODES.md file with the documentation of message codes and
bumps the log module version.

0.9.2: Version bump

25 Feb 05:40
Compare
Choose a tag to compare
0.9.2: Version bump Pre-release
Pre-release

Version bump to work around Go caching.

0.9.1: Better logging

25 Feb 05:39
Compare
Choose a tag to compare
0.9.1: Better logging Pre-release
Pre-release

This release adds logging for service start/stop in pools.

0.9.0: Initial Release

22 Nov 22:25
Compare
Choose a tag to compare
Pre-release

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(),
)

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)