package
1.19.0
Repository: https://github.com/cortexproject/cortex.git
Documentation: pkg.go.dev

# README

Services

This is a Go implementation of services model from Google Guava library.

It provides Service interface (with implementation in BasicService type) and Manager for managing group of services at once.

Main benefits of this model are:

  • Services have well-defined explicit states. Services are not supposed to start any work until they are started, and they are supposed to enter Running state only if they have successfully done all initialization in Starting state.
  • States are observable by clients. Client can not only see the state, but also wait for Running or Terminated state.
  • If more observability is needed, clients can register state listeners.
  • Service startup and shutdown is done asynchronously. This allows for nice parallelization of startup or shutdown of multiple services.
  • Services that depend on each other can simply wait for other service to be in correct state before using it.

Service interface

As the user of the service, here is what you need to know: Each service starts in New state. In this state, service is not yet doing anything. It is only instantiated, and ready to be started.

Service is started by calling its StartAsync method. This will make service transition to Starting state, and eventually to Running state, if starting is successful. Starting is done asynchronously, so that client can do other work while service is starting, for example start more services.

Service spends most of its time in Running state, in which it provides it services to the clients. What exactly it does depends on service itself. Typical examples include responding to HTTP requests, running periodic background tasks, etc.

Clients can stop the service by calling StopAsync, which tells service to stop. Service will transition to Stopping state (in which it does the necessary cleanup) and eventually Terminated state. If service fails in its Starting, Running or Stopping state, it will end up in Failed state instead of Terminated.

Once service is in Terminated or Failed state, it cannot be restarted, these states are terminal.

Full state diagram:

   ┌────────────────────────────────────────────────────────────────────┐
   │                                                                    │
   │                                                                    ▼
┌─────┐      ┌──────────┐      ┌─────────┐     ┌──────────┐      ┌────────────┐
│ New │─────▶│ Starting │─────▶│ Running │────▶│ Stopping │───┬─▶│ Terminated │
└─────┘      └──────────┘      └─────────┘     └──────────┘   │  └────────────┘
                   │                                          │
                   │                                          │
                   │                                          │   ┌────────┐
                   └──────────────────────────────────────────┴──▶│ Failed │
                                                                  └────────┘

API and states and semantics are implemented to correspond to Service class in Guava library.

Manager

Multiple services can be managed via Manager (corresponds to ServiceManager in Guava library). Manager is initialized with list of New services. It can start the services, and wait until all services are running (= "Healthy" state). Manager can also be stopped – which triggers stopping of all its services. When all services are in their terminal states (Terminated or Final), manager is said to be Stopped.

Implementing custom Service

As a developer who wants to implement your own service, there are several possibilities.

Using NewService

The easiest possible way to create a service is using NewService function with three functions called StartingFn, RunningFn and StoppingFn. Returned service will be in New state. When it transitions to Starting state (by calling StartAsync), StartingFn is called. When StartingFn finishes with no error, service transitions to Running state and RunningFn is called. When RunningFn finishes, services transitions to Stopping state, and StoppingFn is called. After StoppingFn is done, service ends in Terminated state (if none of the functions returned error), or Failed state, if there were errors.

Any of the functions can be nil, in which case service simply moves to the next state.

Using NewIdleService

"Idle" service is a service which needs to run custom code during Starting or Stopping state, but not in Running state. Service will remain in Running state until explicitly stopped via StopAsync.

Example usage is a service that registers some HTTP or gRPC handlers.

Using NewTimerService

Timer service runs supplied function on every tick. If this function returns error, service will fail. Otherwise service will continue calling supplied function until stopped via StopAsync.

Using BasicService struct

All previous options use BasicService type internally, and it is BasicService which implements semantics of Service interface. This struct can also be embedded into custom struct, and then initialized with starting/running/stopping functions via InitBasicService:

type exampleService struct {
	*BasicService

	log []string
	ch  chan string
}

func newExampleServ() *exampleService {
	s := &exampleService{
		ch: make(chan string),
	}
    s.BasicService = NewBasicService(nil, s.collect, nil) // StartingFn, RunningFn, StoppingFn
	return s
}

// used as Running function. When service is stopped, context is canceled, so we react on it.
func (s *exampleService) collect(ctx context.Context) error {
	for {
		select {
		case <-ctx.Done():
			return nil
		case msg := <-s.ch:
			s.log = append(s.log, msg)
		}
	}
}

// External method called by clients of the Service.
func (s *exampleService) Send(msg string) bool {
	ctx := s.ServiceContext() // provided by BasicService. Not part of Service interface.
	if ctx == nil {
		// Service is not yet started
		return false
	}
	select {
	case s.ch <- msg:
		return true
	case <-ctx.Done():
		// Service is not running anymore.
		return false
	}
}

Now serv is a service that can be started, observed for state changes, or stopped. As long as service is in Running state, clients can call its Send method:

s := newServ()
s.StartAsync(context.Background())
s.AwaitRunning(context.Background())
// now collect() is running
s.Send("A")
s.Send("B")
s.Send("C")
s.StopAsync()
s.AwaitTerminated(context.Background())
// now service is finished, and we can access s.log

After service is stopped (in Terminated or Failed state, although here the "running" function doesn't return error, so only Terminated state is possible), all collected messages can be read from log field. Notice that no further synchronization is necessary in this case... when service is stopped and client has observed that via AwaitTerminated, any access to log is safe.

(This example is adapted from unit tests in basic_service_test.go)

This may seem like a lot of extra code, and for such a simple usage it probably is. Real benefit comes when one starts combining multiple services into a manager, observe them as a group, or let services depend on each other via Await methods.

# Functions

DescribeService returns name of the service, if it has one, or returns string representation of the service.
NewBasicService returns service built from three functions (using BasicService).
NewIdleService initializes basic service as an "idle" service -- it doesn't do anything in its Running state, but still supports all state transitions.
NewListener provides a simple way to build service listener from supplied functions.
NewManager creates new service manager.
NewManagerListener provides a simple way to build manager listener from supplied functions.
NewTimerService runs iteration function on every interval tick.
StartAndAwaitRunning starts the service, and then waits until it reaches Running state.
StartManagerAndAwaitHealthy starts the manager (which in turns starts all services managed by it), and then waits until it reaches Running state.
StopAndAwaitTerminated asks service to stop, and then waits until service reaches Terminated or Failed state.
StopManagerAndAwaitStopped asks manager to stop its services, and then waits until manager reaches the stopped state or context is stopped.

# Constants

Failed: Service has failed in Starting, Running or Stopping state.
New: Service is new, not running yet.
Running: Service is fully running now.
Starting: Service is starting.
Stopping: Service is shutting down.
Terminated: Service has stopped successfully.

# Structs

BasicService implements contract of Service interface, using three supplied functions: StartingFn, RunningFn and StoppingFn.
FailureWatcher waits for service failures, and passed them to the channel.
Manager is initialized with a collection of services.

# Interfaces

Listener receives notifications about Service state changes.
ManagerListener listens for events from Manager.
NamedService extends Service with a name.
Service defines interface for controlling a service.

# Type aliases

OneIteration is one iteration of the timer service.
RunningFn function is called when service enters Running state.
StartingFn is called when service enters Starting state.
State of the service.
StoppingFn function is called when service enters Stopping state.