This the multi-page printable view of this section.Click here to print.

Return to the regular view of this page.

Services

Table of Contents

A Service is the core entity managed by the microkernel which will provide it with lifecycle support and dependency management.

When a service is deployed it becomes managed by the kernel and follow a strict lifecycle:

Lifecycle Description Function
Inject Resolve dependencies on other services, Declare command line flags field injection
Init Init(*Kernel) error
PostInit Allow checks after init, e.g. command line arguments are correct PostInit() error
Start Allows a service to start, open files, databases etc. Start()
Run Allows a service to perform a task.
This is deprecated as tasks should be performed using the task worker queue instead.
Run() error
Stop Allows a service to free any resources as it shuts down. Stop()

The details of each lifecycle stage is described in full in the following sections, however every one of them is optional as a service does not require any of them to be implemented for a service to be deployed.

The Inject lifecycle was introduced in V1.1.0 and is an alternative to Init although both can be used under certain cirumstances.

v1.1.0

As of version 1.1.0 a service can be any struct. This struct would, when deployed within the kernel become a singleton instance available for injection into other services.

Lifecycle handling can be performed by means of the lifecycle interfaces described below.

Before v1.1.0

Before version 1.1.0 a service consisted of a struct type which implemented the Service interface and optionally any of the lifecycle interfaces.

The Service interface required the Name() function which provided the unique name for this service:

1type Service interface {
2    Name() string
3}

The Name() function must return a unique identifier for your service. Usually this can be the services name, just as long as it's unique with any other services deployed within the kernel.

For Example, a bare minimal Service can be defined

1type MyService struct {
2}
3
4func (s *MyService) Name() string {
5    return "MyService
6}

With this basic definition this new service can now be deployed into the kernel. As this example has no lifecycle functions defined the kernel will just deploy it and make it available to any service that requires access to it.

1 - Naming services

When a service is deployed, it is deployed using a unique name. It is this name which indicates if the service has already been deployed when dependency injection is performed.

As of version 1.1.0 a service can be any struct so by default it would be assigned a unique name based on it's package path and type name providing a unique name for it's type.

Alternatively the service can have the Name() string function which will provide the service's name.

For applications using v1.1.0 or later it's advised to not implement Name() and leave the naming to the Kernel. The Name() function will still be supported as it has uses for specialist use-cases.

Before v1.1.0

In kernel's prior to v1.1.0 the Name() function was mandatory and was the only means of providing a unique name to a service.

2 - Dependencies

A service can have dependencies on other services. During startup, when a service declares it has a dependency then that service will be started before it's dependent. This ensures that a services' functionality is in a valid state during the lifetime of the application.

Dependencies are declared in a service by either injection or in the Init lifecycle phase.

The the following example we have an application with 4 services. Service A depends on B & C, B on C and C with D. This relationship is shown with the black arrows.

Dependency tree

The red arrows show dependencies which are invalid as they create circular dependency which is described below.

Circular dependencies

The kernel does not allow circular dependencies.

In the example above we have four services called A, B, C and D. The black arrows show the valid dependencies but the red arrows show illegal dependencies - that is, a dependency that cannot be made as it would create a circular dependency.

The reason this is an issue is that, when a circular dependency exists the kernel cannot determine the correct starting order.

e.g. if C depends on A, but A indirectly depends on C via B which one should start first?

If this situation happens during the injection or init lifecycle phases the kernel itself will stop returning an error.

3 - Service Dependency Injection

The first lifecycle stage a service goes through is injection. Here a service can declare any other services it has a dependency on or any command line flags it requires by use of field tags within its structure.

For example, here we have a simple service showing the possible combinations available by the Injection lifecycle:

1type Example struct {
2  config  *conf.Config `kernel:"inject"`
3  worker  task.Queue   `kernel:"worker"`
4  _       *PostCSS     `kernel:"inject"`
5  server  *bool        `kernel:"flag,s,Run hugo in server mode"`
6}

Service dependencies

A service can declare a dependency against another service by including th e kernel:"inject" tag against a field of the type of the service.

In the above example we have the field config which has the type *conf.Config. As that field has the tag then the kernel will inject that service's pointer into that field. When the service starts, it will be started before this one.

Unreferenced Service dependencies

An unreferenced dependency is a dependency on another service, but we won't call that service directly. An example of this is a webserver - we need the service to be running whilst this service is running, but we won't call the service directly, e.g. we might make an HTTP connection to it but not via code.

In the above example, the PostCSS service is one of these. We want that service to be deployed however we won't be using it directly so the field name is _. This ensures that service is deployed but not injected into the struct.

Worker Task Queue

The worker task queue is a kernel service allowing for tasks to be queued and prioritised for execution. It's injected using the kernel:"worker" tag.

Command line flags

Command line flags can be injected using the kernel:"flag" tag. This is a composite tag consisting of up to 4 comma separated fields. The first field is always flag.

To inject a flag, simply create a field with its type being one of: *bool, *string, *int, *int64 or *float64. Any other type will cause the service to fail.

The tag takes the following format: flag,{name,{description,{default}}}

name
The name of this flag. It will have '-' prepended to it when used on the command line.
If absent or "" then this will take the name of the field being tagged.
description
The description for this flag, used when listing the available flags on the command line.
If absent or "" then this will take the value used for the flag name.
default
The default value for this flag.
If absent or "" then this will take a suitable default for the type: "" for string, false for boolean, 0 for integers or 0.0 for floats.

4 - Initialise & Service Dependencies

The second lifecycle stage a service goes through is init. Here a service can declare any other services it has a dependency on or any command line flags it requires.

Since v1.1.0 the Init lifecycle has been replaced by Injection for most purposes.

There are some use-cases where Init is still useful but for most purposes you will find Injection is neater and easier to maintain with less boilerplate code.

To do this the service needs to implement the InitialisableService interface:

1type InitialisableService interface {
2  Init(*Kernel) error
3}

If an error is returned from this Init() method the Kernel will exit immediately returning that error.

A service must never call a function in another service from inside the Init() method, nor create any external resources like go routines or open files.

Doing so could call a service which has not yet been initialised and leave resources open if the kernel exits due to an error.

Command line flags

To add a simple flag to a service we simply use the flag package to create the flag from within the Init function.

 1package hugo
 2
 3import (
 4    "flag"
 5    "github.com/peter-mount/documentation/tools/util"
 6    "github.com/peter-mount/go-kernel"
 7)
 8
 9// Hugo runs hugo
10type Hugo struct {
11    server *bool // true to run Hugo in server mode
12}
13
14func (h *Hugo) Name() string {
15    return "hugo"
16}
17
18func (h *Hugo) Init(_ *kernel.Kernel) error {
19    h.server = flag.Bool("s", false, "Run hugo in server mode")
20    return nil
21}

Once the kernel has completed the Init stage we can then reference the flag's value as it would be set with the command line flag from the command line.

Service dependencies

A service can declare a dependency against another service by using the Kernel instance passed to the Init method. The passed instance is only valid for this call, and you should never store it or attempt to use it outside of the Init lifecycle stage.

There are two types of dependencies. The most common one is where you want to use one service from another. To do this you need to use the AddService() function in the Kernel instance passed to Init().

func (k *Kernel) AddService(s Service) (Service, error)

This function accepts a single instance of the service you require. The function will then either return the instance the service that's been deployed in the kernel which you can then cast and store for later use.

If an error occurs then AddService() will return that error. You must exit the Init() function immediately, returning that error.

For example:

 1package hugo
 2
 3import (
 4    "context"
 5    "github.com/peter-mount/go-kernel"
 6)
 7
 8type Webserver struct {
 9    config *Config // Config
10}
11
12func (w *Webserver) Name() string {
13    return "webserver"
14}
15
16func (w *Webserver) Init(k *kernel.Kernel) error {
17    service, err := k.AddService(&Config{})
18    if err != nil {
19        return err
20    }
21    w.config = service.(*Config)
22
23    return nil
24}

The instance you pass to AddService() may not be the same one returned if that service already exists in the kernel.

The key here is the string the Name() function returns. As that must be unique it is used as the unique identifier within the kernel for the service.

As such the lookup follows the following rules:

  • If it already exists then the existing entry will be returned.
  • If it does not exist then the kernel will perform the following in sequence:
    1. If the new Service implements InitialisableService then it's Init() function will be called so that it can add its own dependencies which will then deploy before it.
    2. The new service is finally added to the kernel and the instance you passed to the function will be returned.

If the kernel has a service with the same name defined but of a different type then the cast will cause a panic stopping the kernel.

Unreferenced Service dependencies

The Kernel instance has a second function available, DependsOn(). This is rarely used but is a convenience function where you declare that you depend on one or more services to exist but don't actually want a reference to them.

func (k *Kernel) DependsOn(services ...Service) error

For example, you might have a service that requires a webserver to be running, but you don't need to directly link to it as you would be making http calls to it instead.

 1package pdf
 2
 3import (
 4    "github.com/peter-mount/documentation/tools/hugo"
 5    "github.com/peter-mount/go-kernel"
 6)
 7
 8// PDF tool that handles the generation of PDF documentation of a "book"
 9type PDF struct {
10    config *hugo.Config // Config
11    chromium *hugo.Chromium // Chromium browser
12}
13
14func (p *PDF) Name() string {
15    return "PDF"
16}
17
18func (p *PDF) Init(k *kernel.Kernel) error {
19    service, err := k.AddService(&hugo.Config{})
20    if err != nil {
21        return err
22    }
23    p.config = service.(*hugo.Config)
24
25    service, err = k.AddService(&hugo.Chromium{})
26    if err != nil {
27        return err
28    }
29    p.chromium = service.(*hugo.Chromium)
30
31    // We need a webserver & must run after hugo
32    return k.DependsOn(&hugo.Webserver{}, &hugo.Hugo{})
33}

Here we depend on two services which we store a reference to use them, but we also require two others to be deployed and started before this service.

If an error occurs then DependsOn() will return that error. You must exit Init() returning that error.

5 - PostInit

After the Init stage the kernel enters PostInit. Here the command flags have been parsed so any reference to them are now valid. Any Service's which implements the PostInitialisableService service will have their PostInit() function called so that they can check they are in a valid state.

1type PostInitialisableService interface {
2    PostInit() error
3}

This stage is provided to allow services to stop the kernel if they are in an invalid state before any Service has been started.

For example, if the service created command line flags then it can check that they are valid.

A service must never call a function in another service from inside the PostInit() method, nor create any external resources like go routines or open files.

Doing so could call a service which has not yet been initialised and leave resources open if the kernel exits due to an error.

Example: Checking command line flags are valid failing if it's not been set

 1type Config struct {
 2    configFile *string
 3}
 4
 5func (a *Config) Name() string {
 6	return "Config"
 7}
 8
 9func (a *Config) Init(k *kernel.Kernel) error {
10	a.configFile = flag.String("c", "", "The config file to use")
11
12	return nil
13}
14
15func (a *Config) PostInit() error {
16	if *a.configFile == "" {
17        return fmt.Errorf("No default config defined, provide with -c")
18	}
19
20	return nil
21}

6 - Start & Stop a service

After the Init and PostInit stages have completed the deployment order of Service's in the kernel is confirmed and the kernel then enters the Start stage.

Both Start() and Stop() functions are optional. It is perfectly valid for a service to have only one implemented. For example a service may implement Start() and not have a Stop(). Likewise, some services only implement Stop()

Starting a service

During this stage, the kernel checks each Service in the deployment order and checks to see if it implements the StartableService interface. If it does then it calls the services Start() function to allow the service to initialise itself, creating any resources it requires.

1type StartableService interface {
2    Start() error
3}

For example a service can open an external Database, load a configuration file or create a Webserver.

If a service has internal types that require initialisation like maps, then those maps must be initialised with a Start() method.

A service can call any dependency from within the Start() function as those dependencies have already been started.

If an error occurs then the Start() method must close any resources it has already created and then return an error. The kernel will then enter the Stop stage to stop any service that has already started then return that error.

Stopping a service

Once the kernel has started a service in the Start stage, it also checks to see if the service implements the StoppableService interface. If it does it will add that service to an internal stop list which contains those services which have been started and require stopping during shutdown.

During shutdown, the stop list is called in reverse order so that the last service started will be stopped first.

1type StoppableService interface {
2    Stop()
3}

A service can call any dependency within the Stop() function as those dependencies are still running and will be stopped after this service has exited it's Stop() function.

A service cannot create any new resources or go routines from inside the Stop() function. Attempting to do so may silently fail, especially if the kernel is being stopped due to a SIGINT or SIGTERM signal.

7 - Run

Between the Start and Stop stages is Run. In this stage the kernel runs through each deployed service in deployment order and if that Service implements the Runnable interface then it's Run() function is invoked.

1type RunnableService interface {
2    Run() error
3}

If that function returns an error then the kernel stops and enters the Stop stage. Otherwise, it continues with the next deployed service. When all services are checked it then enters the Stop stage.

Run patterns

There are two patterns for the use of this interface.

Command line tools

In this pattern you write the components of your tool as Service's with each component having dependencies to the various Service's it requires to perform some task.

Then, each one of those components implements the Run() function. It can then test to see if it should actually do something (e.g. a command line flag) and just return nil if it should not do anything, otherwise perform its task.

Once all Service's implementing Run() have done their task the tool can then shutdown.

An example of this is the tool that generates this actual document. It consists of a group of RunnableService's which perform various tasks:

  1. Generates any dynamic pages like indices,
  2. Run's hugo to generate the actual html site,
  3. Run's chromium in headless mode to generate each PDF document based on the site.

Daemon

In the Daemon pattern only one Service implements RunnableService. When it's Run() function is called it never returns keeping the kernel and the application alive.

Dependent services might extend the Daemon service by calling a registration function in the Daemon from within their Start() function.

For example the daemon implements a webserver so the dependent services register handlers against specific paths within the website.

An example of this is the rest package included with the kernel. This implements a Webserver Service where you can register handlers against paths served by the server and implement REST actions which are provided by dependent Service's.