service

package module
v0.0.0-...-84d73a6 Latest Latest
Warning

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

Go to latest
Published: May 7, 2023 License: MIT Imports: 9 Imported by: 2

README

NOTE: This is deprecated. I no longer use it regularly, I typically just use some combination of contexts, errgroups, or some small-sclae roll-your-own thing. Not idea, but neither's maintaining something I don't have time for any more.


Goroutine lifecycle management: service

GoDoc

service implements service-like goroutine lifecycle management.

It is intended for use when you need to co-ordinate the state of one or more long-running goroutines and control startup, shutdown and ready signalling.

Key features:

  • Start and halt backgrounded goroutines (services)
  • Check the state of services
  • Wait until a service is "ready" (you decide what "ready" means)
  • context.Context support (service.Context is a context.Context)

Goroutines are supremely useful, but they're a little too primitive by themselves to support the common use-case of a long-lived goroutine that can be started and halted cleanly. Combined with channels, goroutines can perform this function well, but the amount of error-prone channel boilerplate that starts to accumulate in an application with a large number of these long-lived goroutines can become a maintanability nightmare. go-service attempts to solve this problem with the idea of a "Heavy-weight goroutine" that supports common mechanisms for control.

It is loosely based on .NET/Java style thread classes, but with a distinct Go flair.

Here's a very simple example (though the godoc contains MUCH more information):

type MyRunnable struct {}

func (m *MyRunnable) Run(ctx service.Context) error {
	// Set up your stuff:
	t := time.NewTicker()
	defer t.Stop()

	// Notify the Runner that we are 'ready', which will unblock the call
	// Runner.Start().
	// 
	// If you omit this, Start() will never unblock; failing to call Ready()
	// in a Runnable is an error.
	if err := ctx.Ready(); err != nil {
		return err
	}

	// Run the service, awaiting an instruction from the runner to Halt:
	select {
	case <-ctx.Done():
	case t := <-tick:
		fmt.Println(t)
	}

	return nil
}

func run() error {
	runner := service.NewRunner()

	// Ensure that every service is shut down within 10 seconds, or panic
	// if the deadline is exceeded:
	defer service.MustShutdownTimeout(10*time.Second, runner)

	rn := &MyRunnable{}

	// If you want to be notified if the service ends prematurely, attach
	// an EndListener.
	failer := service.NewFailureListener(1)
	svc := service.New("my-service", rn).WithEndListener(failer)

	// Start a service in the background. The call to Start will unblock when
	// MyRunnable.Run() calls ctx.Ready():
	if err := runner.Start(context.TODO(), svc); err != nil {
		return err
	}
	
	after := time.After(10*time.Second)

	select {
	case <-after:
		// Halt a service and wait for it to signal it finished:
		if err := runner.Halt(context.TODO(), svc); err != nil {
			return err
		}

	case err := <-failer.Failures():
		// If something goes wrong and MyRunnable ends prematurely,
		// the error returned by MyRunnable.Run() will be sent to the
		// FailureListener.Failures() channel.
		return err
	}

	return nil
}

Both Start and Halt can accept a context.Context as the first parameter, There are global functions that can help to reduce boilerplate for the context.WithTimeout() scenario:

runner := service.NewRunner()

// Starting with a timeout...
err := service.StartTimeout(5*time.Second, runner, svc)

// ... is functionally equivalent to this:
context, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
err := runner.Start(context, svc)

Both Start and Halt can accept multiple services at once. Start will block until all services have either signalled ready or failed to start, or until the context is Done():

runner := service.NewRunner()
svc1 := service.New("s1", &MyRunnable{})
svc2 := service.New("s2", &MyRunnable{})
err := runner.Start(context.Background(), svc1, svc2)

Rationale

An earlier version of this library separated the Start and Halt mechanics into separate methods in an interface, rather than bundling everything together into the single Run() function, but this was not as effective and the system became significantly easier to follow when the pattern evolved into Run(). Run() gives you access to defer for your teardown at the same place as your resource acquisition, which is much easier to understand.

Testing

On first glance it might look like there are not a lot of tests in here, but look in the servicetest subpackage; there's an absolute truckload in there.

The test runner is a little bit on the messy side at the moment but it does work:

go run test.go [-cover=<file>] -- [options]

test.go handles calling go test for each package with the required arguments. [options] get passed to the child invocations of go test if they are applicable.

If you pass the -cover* arguments after the --, the reports won't be merged. If you pass it to go run test.go before the --, they will be.

If the tester detects any additional goroutines that have not been closed after the tests have succeeded, the stack traces for those goroutines will be dumped and the tester will respond with an error code. This may cause issues on other platforms but it works on OS X and Ubuntu.

The test suite in the servicetest package includes a fuzz tester, disabled by default. To enable it, pass -service.fuzz=true to go run test.go after the --.

When using the fuzz tester, it is a good idea to pass -v as well.

This will fuzz for 10 minutes and print the results:

go run test.go -- -v -service.fuzz=true -service.fuzztime=600s

This will fuzz for 10 seconds, but will only make a randomised decision every 10ms (this is useful to get more contained tests with fewer things happening to try to reproduce errors):

go run test.go -- -v -service.fuzz=true -service.fuzztime=10 -service.fuzzticknsec=10000000

Seed the fuzzer with a particular value:

go run test.go -- -v -service.fuzz=true -service.fuzztime=10 -service.fuzzseed=12345

You should also run the fuzzer with the race detector turned on as well as with it off. This can help flush out different kinds of bugs. Due to a crazy-low limit on the number of goroutines Go will let you start with -race turned on, you will need to limit the number of services that can be created simultaneously so that error doesn't trip:

go run test.go -- -race -v -service.fuzz=true -service.fuzztime=10 -service.fuzzservicelim=200

See how much coverage we get out of the fuzzer alone:

go run test.go -cover=cover.out -- -run=TestRunnerFuzz -service.fuzz=true -service.fuzztime=10 -v

Documentation

Overview

Package service implements service-like goroutine lifecycle management.

It is intended for use when you need to co-ordinate the state of one or more long-running goroutines and control startup and shutdown.

Package service is complemented by the 'github.com/shabbyrobe/go-service/services' package, which provides a global version of a service.Runner for use in simpler applications.

Quick Example

type MyRunnable struct {}

func (m *MyRunnable) Run(ctx service.Context) error {
	// Set up your stuff:
	tick := time.NewTicker()
	defer tick.Stop()

	// Notify the Runner that we are 'ready', which will unblock the call
	// Runner.Start().
	//
	// If you omit this, Start() will never unblock; failing to call Ready()
	// in a Runnable is an error.
	if err := ctx.Ready(); err != nil {
		return err
	}

	// Run the service, awaiting an instruction from the runner to Halt:
	select {
	case <-ctx.Done():
	case t := <-tick.C:
		fmt.Println(t)
	}

	return nil
}

func run() error {
	runner := service.NewRunner()

	// Ensure that every service is shut down within 10 seconds, or panic
	// if the deadline is exceeded:
	defer service.MustShutdownTimeout(10*time.Second, runner)

	rn := &MyRunnable{}

	// If you want to be notified if the service ends prematurely, attach
	// an EndListener.
	failer := service.NewFailureListener(1)
	svc := service.New("my-service", rn).WithEndListener(failer)

	// Start a service in the background. The call to Start will unblock when
	// MyRunnable.Run() calls ctx.Ready():
	if err := runner.Start(context.TODO(), svc); err != nil {
		return err
	}

	after := time.After(10*time.Second)

	select {
	case <-after:
		// Halt a service and wait for it to signal it finished:
		if err := runner.Halt(context.TODO(), svc); err != nil {
			return err
		}

	case err := <-failer.Failures():
		// If something goes wrong and MyRunnable ends prematurely,
		// the error returned by MyRunnable.Run() will be sent to the
		// FailureListener.Failures() channel.
		return err
	}

	return nil
}

Performance

Services are by nature heavier than a regular goroutine; they're up to 10x slower and use more memory. You should probably only use Services when you need to fully control the management of a long-lived goroutine, otherwise they're likely not worth it:

BenchmarkRunnerStart1-4          	  500000	      2951 ns/op	     352 B/op	       6 allocs/op
BenchmarkGoroutineStart1-4       	 5000000	       368 ns/op	       0 B/op	       0 allocs/op
BenchmarkRunnerStart10-4         	  100000	     20429 ns/op	    3521 B/op	      60 allocs/op
BenchmarkGoroutineStart10-4      	  500000	      2933 ns/op	       0 B/op	       0 allocs/op

There are plenty of opportunities for memory savings in the library, but the chief priority has been to get a working, stable and complete API first. I don't plan to start 50,000 services a second in any app I am currently working on, but this is not to say that optimising the library isn't important, it's just not a priority yet. YMMV.

Runnables

Runnables can be created by implementing the Runnable interface. This interface only contains one method (Run), but there are some very important caveats in order to correctly implement it:

type MyRunnable struct {}

func (m *MyRunnable) Run(ctx service.Context) error {
	// This MUST be present in every implementation of service.Runnable:
	if err := ctx.Ready(); err != nil {
		return err
	}

	// You must wait for the signal to Halt. You can also poll
	// ctx.ShouldHalt().
	<-ctx.Done()

	return nil
}

The Run() method will be run in the background by a Runner. The Run() method MUST do the following to be considered valid. Violating any of these rules will result in Undefined Behaviour (uh-oh!):

  • ctx.Ready() MUST be called and error checked properly

  • <-ctx.Done() MUST be included in any select {} block

  • OR... ctx.ShouldHalt() MUST be checked frequently enough that your calls to Halt() won't time out if <-ctx.Done() is not used.

  • If Run() ends before it is halted by a Runner, an error MUST be returned. If there is no obvious application specific error to return in this case, service.ErrServiceEnded MUST be returned.

The Run() method SHOULD do the following:

  • service.Sleep(ctx) should be used instead of time.Sleep(); service.Sleep() is haltable.

Here is an example of a Run() method which uses a select{} loop:

func (m *MyRunnable) Run(ctx service.Context) error {
	if err := ctx.Ready(); err != nil {
		return err
	}
	for {
		select {
		case stuff := <-m.channelOfStuff:
			m.doThingsWithTheStuff(stuff)
		case <-ctx.Done():
			return nil
		}
	}
}

Here is an example of a Run() method which sleeps:

func (m *MyService) Run(ctx service.Context) error {
	if err := ctx.Ready(); err != nil {
		return err
	}
	for !ctx.ShouldHalt() {
		m.doThingsWithTheStuff(stuff)
		service.Sleep(ctx, 1 * time.Second)
	}
	return nil
}

service.RunnableFunc allows you to use a bare function as a Runnable instead of implementing the Service interface:

service.RunnableFunc("My service", func(ctx service.Context) error {
	// valid Run implementation
})

Runners

To start or halt a Runnable, a Runner is required and the Runnable must be wrapped in a service.Service:

runner := service.NewRunner(nil)
rn1, rn2 := &MyRunnable{}, &MyRunnable{}
svc1, svc2 := service.New("s1", rn1), service.New("s2", rn2)

// start svc1 and wait until it is ready:
err := runner.Start(context.TODO(), svc1)

// start svc1 and svc2 simultaneously and wait until both of them are ready:
err := runner.Start(context.TODO(), svc1, svc2)

// start both services, but wait no more than 1 second for them both to be ready:
err := service.StartTimeout(1 * time.Second, runner, svc1, svc2)
if err != nil {
	// You MUST attempt to halt the services if StartTimeout does not succeed:
	service.MustHaltTimeout(1 * time.Second, runner, svc1, svc2)
}

// the above StartTimeout call is equivalent to the following (error handling
// skipped for brevity):
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
err := runner.Start(ctx, svc1, svc2)

// now halt the services we just started, unblocking when both services have
// ended or failed to end:
if err := runner.Halt(context.TODO(), svc1, svc2); err != nil {
	// If Halt() fails, we have probably leaked a resource. If you have no
	// mechanism to recover it, it could be time to crash:
	panic(err)
}

// halt every service currently started in the runner:
err := runner.Shutdown(context.TODO())

// halt every service in the runner, waiting no more than 1 second for them
// to finish halting:
err := service.ShutdownTimeout(1*time.Second, runner)

Contexts

Service.Run receives a service.Context as its first parameter. service.Context implements context.Context (https://golang.org/pkg/context/).

You should use the ctx as the basis for any child contexts you wish to create in your service, as you will then gain access to cancellation propagation from Runner.Halt():

func (s *MyService) Run(ctx service.Context) error {
	if err := ctx.Ready(); err != nil {
		return err
	}

	dctx, cancel := context.WithDeadline(ctx, time.Now().Add(2 * time.Second))
	defer cancel()

	// This service will be "Done" either when the service is halted,
	// or the deadline arrives (though in the latter case, the service
	// will be considered to have ended prematurely)
	<-dctx.Done()

	// If the service wasn't halted (i.e. if the deadline elapsed), we must
	// return an error to satisfy the service.Run contract outlined in the
	// docs:
	return dctx.Err()
}

The rules around when the ctx passed to Run() is considered Done() are different depending on whether ctx.Ready() has been called. If ctx.Ready() has not yet been called, the ctx passed to Run() is Done() if:

  • The service is halted using Runner.Halt()
  • The context passed to Runner.Start() is either cancelled or its deadline is exceeded.

If ctx.Ready() has been called, the ctx passed to Run() is Done() if:

  • The service is halted using Runner.Halt()
  • That's it.

The context passed to Runner.Halt() is not bound to the ctx passed to Run(). The rationale for this decision is that if you need to do things in your service that require a context.Context after Run's ctx is Done() (i.e. when your Runnable is Halting), you are responsible for creating your own context:

func (s *MyService) Run(ctx service.Context) error {
	if err := ctx.Ready(); err != nil {
		return err
	}
	<-ctx.Done()
	return nil
}

The guideline for this may change, but at the moment the best recommendation is to use context.Background() in this case: if you are calling Runner.Halt() and your context deadline runs out, you have lost resources no matter what.

Listeners

Runner takes a list of functional RunnerOptions that can be used to listen to events.

The RunnerOnEnd option allows you to supply a callback which is executed when a service ends:

endFn := func(stage Stage, service *Service, err error) {
	// behaviour you want when a service ends before it is halted
}
r := service.NewRunner(service.RunnerOnEnd(endFn))

OnEnd will always be called if a Runnable's Run() function returns, whether that is because the service failed prematurely or because it was halted. If the service has ended because it was halted, err will be nil. If the service has ended for any other reason, err MUST contain an error.

The RunnerOnError option allows you to supply a callback which is executed when a service calls service.Context.OnError(err). Context.OnError(err) is used when an error occurs that cannot be handled and does not terminate the service. It's most useful for logging:

errorFn := func(stage Stage, service *Service, err error) {
	log.Println(service.Name, err)
}
r := service.NewRunner(service.RunnerOnError(endFn))

You MUST NOT attempt to call your Runner from inside your OnEnd or OnError function. This will cause a deadlock. If you wish to call the runner, wrap your function body in an anonymous goroutine:

endFn := func(stage Stage, service *Service, err error) {
	// Safe:
	go func() {
		err := runner.Start(...)
	}()

	// NOT SAFE:
	err := runner.Start(...)
}

Restarting

All Runnable implementations are restartable by default. If written carefully, it's also possible to start the same Runnable in multiple Runners. Maybe that's not a good idea, but who am I to judge? You might have a great reason.

Some Runnable implementations may wish to explicitly block restart, such as tings that wrap a net.Conn (which will not be available if the service fails). An atomic can be a good tool for this job:

type MyService struct {
	used int32
}

func (m *MyService) Run(ctx service.Context) error {
	if !atomic.CompareAndSwapInt32(&m.used, 0, 1) {
		return errors.New("cannot reuse MyService")
	}
	if err := ctx.Ready(); err != nil {
		return err
	}
	<-ctx.Done()
	return nil
}
Example
msg := make(chan string)

// Runnables must implement service.Runnable:
runnable := RunnableFunc(func(ctx Context) error {
	defer fmt.Println("halted")

	// Set up your stuff:
	after := time.After(10 * time.Millisecond)

	// Notify the Runner that we are 'ready', which will unblock the call
	// Runner.Start().
	//
	// If you omit this, Start() will never unblock; failing to call Ready()
	// in a Runnable is an error.
	if err := ctx.Ready(); err != nil {
		return err
	}

	// Run the service, awaiting an instruction from the runner to Halt:
	for {
		select {
		case <-ctx.Done():
			return nil
		case <-after:
			msg <- "stop me!"
		}
	}

	return nil
})

runner := NewRunner()

// Ensure that every service is shut down within 10 seconds, or panic
// if the deadline is exceeded:
defer MustShutdownTimeout(10*time.Second, runner)

// If you want to be notified if the service ends prematurely, attach
// an EndListener.
failer := NewFailureListener(1)
svc := New("my-service", runnable).WithEndListener(failer)

// Start a service in the background. The call to Start will unblock when
// the Runnable calls ctx.Ready():
if err := runner.Start(nil, svc); err != nil {
	panic(err)
}

select {
case s := <-msg:
	fmt.Println(s)

	// Halt a service and wait for it to signal it finished:
	if err := runner.Halt(context.TODO(), svc); err != nil {
		panic(err)
	}

case err := <-failer.Failures():
	// If something goes wrong and MyRunnable ends prematurely,
	// the error returned by MyRunnable.Run() will be sent to the
	// FailureListener.Failures() channel.
	panic(err)
}
Output:

stop me!
halted

Index

Examples

Constants

View Source
const MinHaltableSleep = 50 * time.Millisecond

MinHaltableSleep specifies the minimum amount of time that you must pass to service.Sleep() if you want the Sleep() to be cancellable from a context. Calls to service.Sleep() with a duration smaller than this will simply call time.Sleep().

Variables

View Source
var ErrServiceEnded = errors.New("service ended")

ErrServiceEnded is a sentinel error used to indicate that a service ended prematurely but no obvious error that could be returned.

It is a part of the public API so that consumers of this package can return it from their own services.

Functions

func Errors

func Errors(err error) []error

func HaltTimeout

func HaltTimeout(timeout time.Duration, runner Runner, services ...*Service) error

func IsAlreadyRunning

func IsAlreadyRunning(err error) bool

func IsEnded

func IsEnded(err error) bool

func IsRunnerNotEnabled

func IsRunnerNotEnabled(err error) bool

func MustHalt

func MustHalt(ctx context.Context, r Runner, services ...*Service)

MustHalt calls Runner.Halt() and panics if it does not complete successfully.

This is a convenience to allow you to halt in a defer if it is acceptable to crash the server if the service does not Halt.

func MustHaltTimeout

func MustHaltTimeout(timeout time.Duration, r Runner, services ...*Service)

MustHaltTimeout calls MustHalt using context.WithTimeout()

func MustShutdown

func MustShutdown(ctx context.Context, r Runner)

MustShutdown calls Runner.Shutdown() and panics if it does not complete successfully.

This is a convenience to allow you to halt in a defer if it is acceptable to crash the server if the runner does not Shutdown.

func MustShutdownTimeout

func MustShutdownTimeout(timeout time.Duration, r Runner)

MustShutdownTimeout calls MustShutdown using context.WithTimeout()

func MustStart

func MustStart(ctx context.Context, runner Runner, services ...*Service)

func MustStartTimeout

func MustStartTimeout(timeout time.Duration, runner Runner, services ...*Service)

func ShutdownTimeout

func ShutdownTimeout(timeout time.Duration, runner Runner) error

func Sleep

func Sleep(ctx context.Context, d time.Duration) (halted bool)

Sleep allows a Runnable to perform an interruptible sleep - it will return early if the Service is halted.

func StartTimeout

func StartTimeout(timeout time.Duration, runner Runner, services ...*Service) error

Types

type Context

type Context interface {
	context.Context

	// Ready MUST be called by all services when they have finished
	// their setup routines and are considered "Ready" to run. If
	// Ready() returns an error, it MUST be immediately returned.
	Ready() error

	// ShouldHalt is used in situations where you can not block listening
	// to <-Done(), but instead must poll to check if Run() should halt.
	// Checking <-Done() in a select{} is preferred, but not always practical.
	ShouldHalt() bool

	// OnError is used to pass all non-fatal errors that do not cause the
	// service to halt prematurely up to the runner's listener.
	OnError(err error)

	// Runner allows you to access the Runner from which the invocation of
	// Run() originated. This lets you start child services in the same runner.
	// It is safe to call any method of this from inside a Runnable.
	Runner() Runner
}

Context is passed to a Service's Run() method. It is used to signal that the service is ready, to receive the signal to halt, or to relay non-fatal errors to the Runner's listener.

All services must either include Context.Done() in their select loop, or regularly poll ctx.ShouldHalt() if they don't make use of one.

service.Context is a context.Context, so you can use it anywhere you would expect to be able to use a context.Context and the thing you are using will be signalled when your service is halted:

func (s *MyService) Run(ctx service.Context) error {
	if err := ctx.Ready(); err != nil {
		return err
	}

	rqCtx, cancel := context.WithDeadline(ctx, time.Now().Add(2 * time.Second))
	defer cancel()

	client := &http.Client{}
	rq, err := http.NewRequest("GET", "http://example.com", nil)
	// ...

	// The child context passed to the request will be "Done" either when
	// the service is halted, or the deadline arrives, so this request will
	// be aborted by the service being Halted:
	rq = rq.WithContext(rqCtx)
	rs, err := client.Do(rq)
	// ...

	<-ctx.Done()

	return nil
}

type EndListener

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

EndListener provides a channel that emits errors when services end unexpectedly, and nil when they end expectedly.

WARNING: this API is not stable.

If the ends channel would block, items are discarded.

If you expect a certain number of ends, you should pass at least that number to cap. If not, you may find that any ends that occur between a service stopping unexpectedly and the channel being read from get dropped.

func NewEndListener

func NewEndListener(cap int) *EndListener

func (*EndListener) AttachEnd

func (e *EndListener) AttachEnd(svc *Service)

func (*EndListener) Ends

func (e *EndListener) Ends() <-chan error

func (*EndListener) ForRunner

func (e *EndListener) ForRunner() RunnerOption

func (*EndListener) OnEnd

func (e *EndListener) OnEnd(stage Stage, service *Service, err error)

func (*EndListener) Send

func (e *EndListener) Send(err error)

Send sends an arbitrary error through the failure channel. It can send nil. err is discarded if Send woudl block.

func (*EndListener) SendNonNil

func (e *EndListener) SendNonNil(err error)

SendNonNil sends an arbitrary error through the failure channel if it is not nil. Use it if you want to mix arbitrary goroutine error handling with service failure.

type Endable

type Endable interface {
	AttachEnd(svc *Service)
}

type Error

type Error interface {
	error

	Name() Name
	// contains filtered or unexported methods
}

func WrapError

func WrapError(err error, svc *Service) Error

type FailureListener

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

FailureListener provides a channel that emits errors when services end unexpectedly.

WARNING: this API is not stable.

If the failure channel would block, errors are discarded.

If you expect a certain number of errors, you should pass at least that number to cap. If not, you may find that any errors that occur between a service stopping unexpectedly and the channel being read from get dropped.

Example
var wg sync.WaitGroup
wg.Add(3) // there are 3 services we should receive ends for

onEnd := func(stage Stage, service *Service, err error) {
	fmt.Println(service.Name, err)
	wg.Done()
}
runner := NewRunner(RunnerOnEnd(onEnd))

// This Runnable will run until it is halted:
runningChild := RunnableFunc(func(ctx Context) error {
	if err := ctx.Ready(); err != nil {
		return err
	}
	<-ctx.Done()
	return nil
})

// This Runnable will fail as soon as it is Ready():
failingChild := RunnableFunc(func(ctx Context) error {
	if err := ctx.Ready(); err != nil {
		return err
	}
	return fmt.Errorf("BOOM!")
})

// This service starts two child services and ensures they are halted
// before it returns. It will fail when the first child service fails:
parent := New("parent", RunnableFunc(func(ctx Context) error {
	failer := NewFailureListener(1)

	s1 := New("s1", runningChild).WithEndListener(failer)
	s2 := New("s2", failingChild).WithEndListener(failer)
	if err := ctx.Runner().Start(nil, s1, s2); err != nil {
		return err
	}
	defer MustHalt(context.Background(), ctx.Runner(), s1, s2)

	if err := ctx.Ready(); err != nil {
		return err
	}

	select {
	case <-ctx.Done():
		return nil
	case err := <-failer.Failures():
		return err
	}
}))

MustStartTimeout(1*time.Second, runner, parent)
wg.Wait()
Output:

s2 BOOM!
s1 <nil>
parent BOOM!

func NewFailureListener

func NewFailureListener(cap int) *FailureListener

func (*FailureListener) AttachEnd

func (f *FailureListener) AttachEnd(svc *Service)

func (*FailureListener) Failures

func (f *FailureListener) Failures() <-chan error

func (*FailureListener) ForRunner

func (f *FailureListener) ForRunner() RunnerOption

func (*FailureListener) OnEnd

func (f *FailureListener) OnEnd(stage Stage, service *Service, err error)

func (*FailureListener) Send

func (f *FailureListener) Send(err error)

Send sends an arbitrary error through the failure channel. It can send nil. err is discarded if Send woudl block.

func (*FailureListener) SendNonNil

func (f *FailureListener) SendNonNil(err error)

SendNonNil sends an arbitrary error through the failure channel if it is not nil. Use it if you want to mix arbitrary goroutine error handling with service failure.

type Name

type Name string

func (Name) Append

func (s Name) Append(name string) Name

func (Name) AppendName

func (s Name) AppendName(name Name) Name

func (Name) AppendUnique

func (s Name) AppendUnique() Name

func (Name) Empty

func (s Name) Empty() bool

type OnEnd

type OnEnd func(stage Stage, service *Service, err error)

type OnError

type OnError func(stage Stage, service *Service, err error)

type Runnable

type Runnable interface {
	// Run the service, blocking the caller until the service is complete.
	// ready MUST not be nil. ctx.Ready() MUST be called.
	//
	// If Run() ends because <-ctx.Done() has yielded, you MUST return nil.
	// If Run() ends for any other reason, you MUST return an error.
	Run(ctx Context) error
}

type RunnableFunc

type RunnableFunc func(ctx Context) error

RunnableFunc allows you to create a Runnable from a Closure, similar to http.HandlerFunc:

service.New("my-runnable-func", func(ctx service.Context) error {
	if err := ctx.Ready() {
		return err
	}
	<-ctx.Done()
	return nil
})

func (RunnableFunc) Run

func (r RunnableFunc) Run(ctx Context) error

type Runner

type Runner interface {
	// Start one or more services in this runner and block until they are Ready.
	//
	// Start will unblock when all services have either signalled Ready or have
	// returned an error indicating they have failed to start, or when the context
	// provided to Start() is Done().
	//
	// An optional context can be provided via ctx; this allows cancellation to
	// be declared outside the Runner. You may provide a nil Context.
	//
	// If ctx signals Done(), you should try to Halt() the service as you may
	// leak a goroutine - the service may have become Ready() after you stopped
	// waiting for it.
	//
	Start(ctx context.Context, services ...*Service) error

	// Halt one or more services that have been started in this runner and block
	// until they have halted.
	//
	// Halt will unblock when all services have finished halting. If Halt returns
	// an error, one or more of the services may have failed to halt. If a service
	// fails to Halt(), you may have leaked a goroutine and you should probably
	// panic().
	//
	// You may Halt() a service in any state. If the service is already Halted
	// or has already Ended, Halt will immediately succeed for that service.
	//
	// An optional context can be provided via ctx; this allows cancellation to
	// be declared outside the Runner. You may provide a nil Context.
	//
	// If the context is cancelled before the service halts, you may have leaked
	// a goroutine; there is no way for a service lost in this manner to be
	// recovered using go-service, you will need to build in your own recovery
	// mechanisms if you want to handle this condition. In practice, a
	// cancelled 'halt' is probably a good time to panic(), but your specific
	// application may be able to tolerate some goroutine leaks until you can
	// fix the issue.
	Halt(ctx context.Context, services ...*Service) error

	// Shutdown halts all services started in this runner and prevents new ones
	// from being started. It will block until all services have Halted.
	//
	// If any service fails to halt, err will contain an error for each service
	// that failed, accessible by calling service.Errors(err). n will contain
	// the number of services successfully halted.
	//
	// An optional context can be provided via ctx; this allows cancellation to
	// be declared outside the Runner. You may provide a nil Context, but this is
	// not recommended as your application may block indefinitely.
	//
	// It is safe to call Shutdown multiple times.
	Shutdown(ctx context.Context) (err error)

	// Enable resumes a Shutdown runner.
	Enable() error

	// Suspend prevents new services from being started in this Runner, but
	// does not shut down existing services.
	Suspend() error

	RunnerState() RunnerState

	State(svc *Service) State

	// Services returns the list of services running at the time of the call.
	// time of the call. If StateQuery is provided, only the matching services
	// are returned.
	//
	// Pass limit to restrict the number of returned results. If limit is <= 0,
	// all matching services are returned.
	//
	// You can instruct States to allocate into an existing slice by passing
	// it in. You should replace it with the return value in case it needs
	// to grow.
	Services(state State, limit int, into []ServiceInfo) []ServiceInfo
}

Runner Starts, Halts and manages Services.

func NewRunner

func NewRunner(opts ...RunnerOption) Runner

type RunnerOption

type RunnerOption func(rn *runner)

func RunnerOnEnd

func RunnerOnEnd(cb OnEnd) RunnerOption

RunnerOnEnd supplies a default OnEnd callback for use in a Runner.

Your OnEnd function will always be called if a Runnable's Run() function returns, whether that is because the service failed prematurely or because it was halted. If the service has ended because it was halted, err will be nil. If the service has ended for any other reason, err MUST contain an error.

The Runner will be locked while your callback is in progress. You should return as quickly as possible. It is not safe to call methods on a Runner from the OnEnd callback; doing so may cause your application to deadlock. If you need to call the Runner, wrap the body of your OnEnd in an anonymous goroutine:

RunnerOnEnd(func(stage Stage, service *Service, err error) {
	// Safe:
	go func() {
		runner.Start(...)
	}()

	// NOT SAFE:
	runner.Start(...)
})

func RunnerOnError

func RunnerOnError(cb OnError) RunnerOption

func RunnerOnState

func RunnerOnState(ch chan<- StateChange) RunnerOption

type RunnerState

type RunnerState int
const (
	RunnerEnabled   RunnerState = 0
	RunnerSuspended RunnerState = 1
	RunnerShutdown  RunnerState = 2
)

type Service

type Service struct {
	Name     Name
	Runnable Runnable

	// OnEnd allows you to supply a callback which will be executed whenever a
	// Runnable's Run() function returns.
	//
	// If the Runnable has returned because it was halted, err will be nil. If
	// the service has ended for any other reason, err MUST contain an error.
	//
	// If the Runner has a default OnEnd function (see RunnerOnEnd), both
	// callbacks will be called.
	//
	// It is not safe to call methods on a Runner from the OnEnd callback; doing so
	// may cause your application to deadlock. If you need to call the Runner, wrap
	// the body of your OnEnd in an anonymous goroutine:
	//
	//	svc.OnEnd = func(stage Stage, service *Service, err error) {
	//		// Safe:
	//		go func() {
	//			runner.Start(...)
	//		}()
	//
	//		// NOT SAFE:
	//		runner.Start(...)
	//	}
	//
	OnEnd OnEnd

	// OnStateChange allows you to receive notifications when the state
	// of a service changes. The Runner will drop state changes if this
	// channel is not able to receive them, so supply a big buffer if
	// that concerns you.
	OnState chan StateChange
}

Service wraps a Runnable with common properties.

func New

func New(n Name, r Runnable) *Service

func (*Service) WithEndListener

func (s *Service) WithEndListener(endable Endable) *Service

func (*Service) WithOnEnd

func (s *Service) WithOnEnd(onEnd OnEnd) *Service

type ServiceInfo

type ServiceInfo struct {
	State   State
	Service *Service
}

type Stage

type Stage int
const (
	StageReady Stage = 1
	StageRun   Stage = 2
)

type State

type State int
const (
	NoState  State = 0
	AnyState State = 0 // AnyState is a symbolic name used to make queries more readable.
	Halted   State = 1 << iota
	Starting
	Started
	Halting
	Ended
)

State should not be used as a flag by external consumers of this package.

func (State) IsRunning

func (s State) IsRunning() bool

func (State) Match

func (s State) Match(q State) bool

func (State) String

func (s State) String() string

type StateChange

type StateChange struct {
	Service  *Service
	From, To State
}

Directories

Path Synopsis
internal
Package services contains a set of convenience functions for managing a global service.Runner.
Package services contains a set of convenience functions for managing a global service.Runner.
Package servicetest contains tools and tests for the service package.
Package servicetest contains tools and tests for the service package.
Package serviceutil contains useful experiments and utilities that have not yet reached the level of robustness required to be part of the service package.
Package serviceutil contains useful experiments and utilities that have not yet reached the level of robustness required to be part of the service package.

Jump to

Keyboard shortcuts

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