bandmaster

package module
v1.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 2, 2018 License: Apache-2.0 Imports: 8 Imported by: 0

README

Bandmaster

BandMaster Status Build Status Coverage Status GoDoc

BandMaster is a simple and easily extendable Go library for managing runtime services & dependencies such as reliance on external datastores/APIs/MQs/custom-things via a single, consistent set of APIs.

It provides a fully tested & thread-safe package that implements some of the most-commonly needed features when dealing with 3rd-party clients, including but not limited to:

  • consistent, type-safe, environment-based configuration for everything
  • configurable number of retries & support for exponential backoff to recover from temporary initialization failures
  • a blocking status API (via chan + select{}) so you can wait for one or more services to be ready
  • designed to ease the creation & integration of custom services
  • automatic parallelization & synchronization of the boot & shutdown phases
  • dependency-tree semantics to define relationships between services
  • auto-detection of missing & circular dependencies
  • a global, thread-safe service registry so packages and goroutines can safely share clients
  • full support of context for clean cancellation of the boot & shutdown processes (using e.g. signals)
  • idempotent start & stop methods
  • ...and more!

BandMaster comes with a standard library of services including:

Any of these services would be configured and instanciated the exact same way:

	memcachedEnv, _ := bm_memcached.NewEnv("MC_EXAMPLE")
	redisEnv, _ := bm_redis.NewEnv("RD_EXAMPLE")
	cqlEnv, _ := bm_cql.NewEnv("CQL_EXAMPLE")
	natsEnv, _ := bm_nats.NewEnv("NATS_EXAMPLE")
	kafkaEnv, _ := bm_kafka.NewEnv("KAFKA_EXAMPLE")
	es1Env, _ := bm_es1.NewEnv("ES1_EXAMPLE")
	es2Env, _ := bm_es2.NewEnv("ES2_EXAMPLE")
	es5Env, _ := bm_es5.NewEnv("ES5_EXAMPLE")
	sqlEnv, _ := bm_sql.NewEnv("SQL_EXAMPLE")

	m.AddService("mc-1", true, bm_memcached.New(memcachedEnv.Config()))
	m.AddService("rd-1", true, bm_redis.New(redisEnv.Config()))
	m.AddService("cql-1", true, bm_cql.New(cqlEnv.Config()))
	m.AddService("nts-1", true, bm_nats.New(natsEnv.Config()))
	m.AddService("kfk-1", true, bm_kafka.New(kafkaEnv.Config()))
	m.AddService("es1-1", true, bm_es1.New(es1Env.Config()))
	m.AddService("es2-1", true, bm_es2.New(es2Env.Config()))
	m.AddService("es5-1", true, bm_es5.New(es5Env.Config()))
	m.AddService("sql-1", true, bm_sql.New(sqlEnv.Config()))

In addition to these standard implementations, BandMaster provides a straightforward API so that you can easily implement your own services; see this section for more details.


Table of Contents:

Usage

Quickstart

This example shows some basic usage of BandMaster that should cover 99.9% of the use-cases out there:

// build logger with deterministic output for this example
zap.ReplaceGlobals(newLogger())

// get package-level Maestro instance
m := bandmaster.GlobalMaestro()

// get environment or default configuration for memcached & redis
memcachedEnv, _ := bm_memcached.NewEnv("MC_EXAMPLE")
redisEnv, _ := bm_redis.NewEnv("RD_EXAMPLE")

// add a memcached service called 'mc-1' that depends on 'rd-1' which
// does not yet exist
m.AddService("mc-1", true, bm_memcached.New(memcachedEnv.Config()), "rd-1")
// add a memcached service called 'mc-2' with no dependencies
m.AddService("mc-2", false, bm_memcached.New(memcachedEnv.Config()))
// add a memcached service called 'mc-3' that depends on 'mc-2'
m.AddService("mc-3", true, bm_memcached.New(memcachedEnv.Config()), "mc-2")
// add a redis service called 'rd-1' that depends on 'mc-3', and hence
// also indirectly depends on on 'mc-2'
m.AddService("rd-1", true, bm_redis.New(redisEnv.Config()), "mc-3")

// add a final memcached service called 'mc-x' that just directly depends
// on everything else, cannot possibly boot successfully, and has some
// exponential backoff configured
conf := memcachedEnv.Config()
conf.Addrs = []string{"localhost:0"}
m.AddServiceWithBackoff(
  "mc-x", true,
  3, time.Millisecond*100, bm_memcached.New(conf),
  "mc-1", "mc-2", "mc-3", "rd-1")

/* Obviously, memcached instances depending on other memcached instances
 * doesn't make any kind of sense, but that's just for the sake of example
 */

// give it 5sec max to start everything
ctx, _ := context.WithTimeout(context.Background(), time.Second*5)
// once the channel returned by StartAll gets closed, we know for a fact
// that all of our services (minus the ones that returned an error) are
// ready for action
for err := range m.StartAll(ctx) {
  e, ok := errors.Cause(err).(*bandmaster.Error)
  if ok {
    // if the service is marked as required, we should start worrying
    if e.Service.Required() {
      zap.L().Error("couldn't start required service",
        zap.Error(e), zap.String("service", e.Service.Name()))
    } else {
      zap.L().Info("couldn't start optional service",
        zap.Error(e), zap.String("service", e.Service.Name()))
    }
  }
}

// since StartAll's channel is closed, our services must be ready by now
mc1, mc2, mc3 := m.Service("mc-1"), m.Service("mc-2"), m.Service("mc-3")
rd1 := m.Service("rd-1")
for i := 0; i < 3; i++ {
  zap.L().Info("doing stuff with our new services...",
    zap.String("memcacheds", fmt.Sprintf("mc-1:%p mc-2:%p mc-3:%p", mc1, mc2, mc3)),
    zap.String("redis", fmt.Sprintf("rd-1:%p", rd1)))
  time.Sleep(time.Second)
}

// give it 5sec max to stop everything
ctx, _ = context.WithTimeout(context.Background(), time.Second*5)
// once the channel returned by StopAll gets closed, we know for a fact
// that all of our services (minus the ones that returned an error) are
// properly shutdown
for err := range m.StopAll(ctx) {
  zap.L().Info(err.Error())
}

It should output the following when ran, explaining pretty straightforwardly what's actually going on:

{"level":"info","msg":"starting service...","service":"mc-2"}
{"level":"info","msg":"starting service...","service":"rd-1"}
{"level":"info","msg":"starting service...","service":"mc-1"}
{"level":"info","msg":"starting service...","service":"mc-x"}
{"level":"info","msg":"starting service...","service":"mc-3"}
{"level":"info","msg":"service successfully started","service":"'mc-2' [optional]"}
{"level":"info","msg":"service successfully started","service":"'mc-3' [required]"}
{"level":"info","msg":"service successfully started","service":"'rd-1' [required]"}
{"level":"info","msg":"service successfully started","service":"'mc-1' [required]"}
{"level":"info","msg":"service failed to start, retrying in 100ms...","service":"mc-x","error":"dial tcp 127.0.0.1:0: connect: can't assign requested address","attempt":1}
{"level":"info","msg":"service failed to start, retrying in 200ms...","service":"mc-x","error":"dial tcp 127.0.0.1:0: connect: can't assign requested address","attempt":2}
{"level":"warn","msg":"service failed to start","service":"mc-x","error":"dial tcp 127.0.0.1:0: connect: can't assign requested address","attempt":3}
{"level":"error","msg":"couldn't start required service","error":"`mc-x`: service failed to start: dial tcp 127.0.0.1:0: connect: can't assign requested address","service":"mc-x"}
{"level":"info","msg":"doing stuff with our new services...","memcacheds":"mc-1:0xc4200e85a0 mc-2:0xc4200e85e0 mc-3:0xc4200e8620","redis":"rd-1:0xc420011090"}
{"level":"info","msg":"doing stuff with our new services...","memcacheds":"mc-1:0xc4200e85a0 mc-2:0xc4200e85e0 mc-3:0xc4200e8620","redis":"rd-1:0xc420011090"}
{"level":"info","msg":"doing stuff with our new services...","memcacheds":"mc-1:0xc4200e85a0 mc-2:0xc4200e85e0 mc-3:0xc4200e8620","redis":"rd-1:0xc420011090"}
{"level":"info","msg":"stopping service...","service":"mc-3"}
{"level":"info","msg":"stopping service...","service":"mc-x"}
{"level":"info","msg":"stopping service...","service":"mc-2"}
{"level":"info","msg":"stopping service...","service":"mc-1"}
{"level":"info","msg":"service successfully stopped","service":"'mc-2' [optional]"}
{"level":"info","msg":"stopping service...","service":"rd-1"}
{"level":"info","msg":"service successfully stopped","service":"'mc-3' [required]"}
{"level":"info","msg":"service successfully stopped","service":"'rd-1' [required]"}
{"level":"info","msg":"service successfully stopped","service":"'mc-1' [required]"}
{"level":"info","msg":"service successfully stopped","service":"'mc-x' [required]"}

You can run this example yourself by typing the following commands:

$ docker-compose -f test/docker-compose.yml up -d memcached redis
$ go run example/main.go
Implementing a custom service

The simplest way to implement a new service is to copy & modify one of the already existing ones. Each service is implemented in its own subpackage, all of which you can find in the services/ directory.

The nats package is a good starting point for your future implementations.

Services are always composed of 3 files:

  • env.go
    This defines an Env structure that handles all of the service's configuration and can easily be converted to its native configuration type (e.g. nats.Options).
    Make sure to always provide sane defaults that will ease development in a local environment.
  • service.go
    It defines the actual Service structure that plugs into BandMaster's machinery by pseudo-inheriting (embedding) from bandmaster.ServiceBase.
    This also defines the self-explanatory Start & Stop methods, as well as a Client (or similarly named) method that allows the caller to retrieve the actual client managed by the service (e.g. nats.Conn).
  • service_test.go
    Finally, this plugs the newly implemented service into the generic BandMaster test suite via services.TestService_Generic.
    These tests will make sure that the service's behavior is consistent with every other services' supported by BandMaster, in any circumstances.
Error handling

BandMaster uses the pkg/errors package to handle error propagation throughout the call stack; please take a look at the related documentation for more information on how to properly handle these errors.

Logging

BandMaster does some logging whenever a service or one of its dependency undergoes a change of state or if anything went wrong; for that, it uses the global logger from Uber's Zap package. You can thus control the behavior of BandMaster's logger however you like by calling zap.ReplaceGlobals at your convenience.

For more information, see Zap's documentation.

Contributing

Contributions of any kind are welcome; especially additions to the library of Service implementations, and improvements to the env-based configuration of existing services.

BandMaster is pretty-much frozen in terms of features; if you still find it to be lacking something, please file an issue to discuss it first. Also, do not hesitate to open an issue if some piece of documentation looks either unclear or incomplete to you, nay is just missing entirely.

Code contributions must be thoroughly tested and documented.

Running tests
$ docker-compose -f docker-compose.yml up -d
$ make test

Authors

See AUTHORS for the list of contributors.

License License

The Apache License version 2.0 (Apache2) - see LICENSE for more details.

Copyright (c) 2017 Zenly hello@zen.ly @zenlyapp

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Error

type Error struct {
	Kind ErrorKind

	Service     Service
	ServiceName string
	ServiceErr  error

	Dependency   string
	Parent       string
	CircularDeps []string
}

Error is an error returned by BandMaster.

Only the `Kind` field is guaranteed to be defined at all times; the rest may or may not be set depending on the kind of the returned error.

func (*Error) Error

func (e *Error) Error() string

type ErrorKind

type ErrorKind int

ErrorKind enumerates the possible kind of errors returned by BandMaster.

const (
	/* common */
	ErrUnknown ErrorKind = iota // unknown error

	/* fatal */
	ErrServiceAlreadyExists       ErrorKind = iota // service already exists
	ErrServiceWithoutBase         ErrorKind = iota // service must inherit form ServiceBase
	ErrServiceDependsOnItself     ErrorKind = iota // service depends on its own self
	ErrServiceDuplicateDependency ErrorKind = iota // service has duplicate dependencies

	/* dependencies */
	ErrDependencyMissing     ErrorKind = iota // no such dependency
	ErrDependencyCircular    ErrorKind = iota // circular dependencies detected
	ErrDependencyUnavailable ErrorKind = iota // dependency failed to start
	ErrParentUnavailable     ErrorKind = iota // parent failed to shutdown

	/* runtime */
	ErrServiceStartFailure ErrorKind = iota // service failed to start
	ErrServiceStopFailure  ErrorKind = iota // service failed to stop
)

type Maestro

type Maestro struct {
	// contains filtered or unexported fields
}

A Maestro registers services and their dependencies, computes depency trees and properly handles the boot & shutdown processes of these trees.

It does not care the slightest as to what happens to a service during the span of its lifetime; only boot & shutdown processes matter to it.

func GlobalMaestro

func GlobalMaestro() *Maestro

GlobalMaestro returns package-level Maestro; this is what you want to use most of the time. This is thread-safe.

func NewMaestro

func NewMaestro() *Maestro

NewMaestro instanciates a new local Maestro.

Unless you're facing special circumstances, you might just as well use the already instanciated, package-level Maestro.

func ReplaceGlobalMaestro

func ReplaceGlobalMaestro(m *Maestro) *Maestro

ReplaceGlobalMaestro replaces the package-level Maestro with your own. This is thred-safe.

func (*Maestro) AddService

func (m *Maestro) AddService(name string, req bool, s Service, deps ...string)

AddService registers a Service with the Maestro.

The direct dependencies of this service can be specified using the names with which they were themselves registered. You do NOT need to specify its indirect dependencies. You're free to indicate the names of dependencies that haven't been registered yet: the final dependency-tree is only computed at StartAll() time.

The specified `req` boolean marks the service as required; which can be a helpful indicator as to whether you can afford to ignore some errors that might happen later on.

func (*Maestro) AddServiceWithBackoff

func (m *Maestro) AddServiceWithBackoff(
	name string, req bool,
	maxRetries uint, initialBackoff time.Duration,
	s Service, deps ...string,
)

AddServiceWithBackoff is an enhanced version of AddService that allows to configure exponential backoff in case of failures to boot.

`maxRetries` defines the maximum number of times that the service will try to boot; while `initialBackoff` specifies the initial sleep-time before each retry.

func (*Maestro) Service

func (m *Maestro) Service(name string) Service

Service returns the service associated with the specified name, whether it is marked as ready or not. It it thread-safe.

If no such service exists, this method will return a nil value.

func (*Maestro) ServiceReady

func (m *Maestro) ServiceReady(ctx context.Context, name string) Service

Service returns the service associated with the specified name, but blocks until it is marked as ready before returning. It it thread-safe.

If no such service exists, this method will immediately return a nil value.

Cancelling the specified context by any mean will unblock the method and return a nil value.

func (*Maestro) StartAll

func (m *Maestro) StartAll(ctx context.Context) <-chan error

StartAll starts all the dependencies referenced by this Maestro in the order required by the dependency-tree. Services that don't depend on one another will be started in parallel.

StartAll is blocking: on success, the returned channel is closed and hence will return nil values indefinitely; on failure, one or more errors are returned, then the channel gets closed. In either case, once this channel is closed, except for those that returned an error, all services can be safely considered up & running.

If the specified context were to get cancelled for any reason (say a caught SIGINT for example), the entire boot process will be cleanly cancelled too. Note that some of the services may well have been successfully started before the cancellation event actually hit the pipeline though: thus you should probably call StopAll() after cancelling a boot process.

Before actually doing anything, StartAll will check for missing and/or circular dependencies; if any such thing were to be detected, an error will be pushed on the returned channel and NOTHING will get started.

func (*Maestro) StopAll

func (m *Maestro) StopAll(ctx context.Context) <-chan error

StopAll stops all the services managed by the Maestro in the reversed order in which they were started. Services that don't depend on one another will be stopped in parallel.

StopAll is blocking: on success, the returned channel is closed and hence will return nil values indefinitely; on failure, one or more errors are returned, then the channel gets closed. In either case, once this channel is closed, except for those that returned an error, all services can be safely considered properly shutdown.

If the specified context were to get cancelled for any reason (say a caught SIGINT for example), the entire stop process will be cleanly cancelled too. Note that some of the services may well have been successfully stopped before the cancellation event actually hit the pipeline though.

type Service

type Service interface {
	Start(ctx context.Context, deps map[string]Service) error
	Stop(ctx context.Context) error

	Name() string
	Required() bool
	RetryConf() (uint, time.Duration)

	String() string

	Started() <-chan error
	Stopped() <-chan error
}

Service is the main interface behind Bandmaster, any service that wishes to be operated by a Maestro must implement it.

To ease the integration of new services into the system, a ServiceBase class that you can pseudo-inherit from (i.e. embed) is provided and will automagically fill in most of the boilerplate required to implement the Service interface. See any service implementation in "services/" folder for examples of this.

type ServiceBase

type ServiceBase struct {
	// contains filtered or unexported fields
}

A ServiceBase implements most of the boilerplate required to satisfy the Service interface. You can, and should, embed it in your service structure to ease its integration in BandMaster. See any service implementation in "services/" folder for examples of this.

func NewServiceBase

func NewServiceBase() *ServiceBase

NewServiceBase returns a properly initialized ServiceBase that you can embed in your service definition.

func (*ServiceBase) Dependencies

func (sb *ServiceBase) Dependencies() map[string]struct{}

Dependencies returns a set of the direct dependencies of the service.

func (*ServiceBase) Name

func (sb *ServiceBase) Name() string

Name returns the name of the service; it is thread-safe.

func (*ServiceBase) Required

func (sb *ServiceBase) Required() bool

Required returns true if the service is marked as required; it is thread-safe.

func (*ServiceBase) RetryConf

func (sb *ServiceBase) RetryConf() (uint, time.Duration)

RetryConf returns the number of retries and the initial value used by the the service for exponential backoff; it is thread-safe.

func (*ServiceBase) Started

func (sb *ServiceBase) Started() <-chan error

Started returns a channel that will get closed once the boot process went successfully. This is thread-safe.

Every failed start attempt will push an error (retries due to exponential backoff are not treated as failed attempts); hence you need to make sure to keep reading continuously on the returned channel or you might block the Maestro otherwise.

func (*ServiceBase) Stopped

func (sb *ServiceBase) Stopped() <-chan error

Stopped returns a channel that will get closed once the shutdown process went successfully. This is thread-safe.

Every failed shutdown attempt will push an error (retries due to exponential backoff are not treated as failed attempts); hence you need to make sure to keep reading continuously on the returned channel or you might block the Maestro otherwise.

func (*ServiceBase) String

func (sb *ServiceBase) String() string

Directories

Path Synopsis
cql
es1
es2
es5
sql

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL