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.