catacomb

package
v4.0.0 Latest Latest
Warning

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

Go to latest
Published: Jan 12, 2024 License: LGPL-3.0 Imports: 9 Imported by: 11

Documentation

Overview

Catacomb leverages tomb.Tomb to bind the lifetimes of, and track the errors of, a group of related workers. It's intended to be close to a drop-in replacement for a Tomb: if you're implementing a worker, the only differences should be (1) a slightly different creation dance; and (2) you can later call .Add(aWorker) to bind the worker's lifetime to the catacomb's, and cause errors from that worker to be exposed via the catacomb. Oh, and there's no global ErrDying to induce surprising panics when misused.

This approach costs many extra goroutines over tomb.v2, but is slightly more robust because Catacomb.Add() verfies worker registration, and is thus safer than Tomb.Go(); and, of course, because it's designed to integrate with the worker.Worker model already common in juju.

Note that a Catacomb is *not* a worker itself, despite the internal goroutine; it's a tool to help you construct workers, just like tomb.Tomb.

The canonical expected construction of a catacomb-based worker is as follows:

type someWorker struct {
    config   Config
    catacomb catacomb.Catacomb
    // more fields...
}

func NewWorker(config Config) (worker.Worker, error) {

    // This chunk is exactly as you'd expect for a tomb worker: just
    // create the instance with an implicit zero catacomb.
    if err := config.Validate(); err != nil {
        return nil, errors.Trace(err)
    }
    w := &someWorker{
        config:   config,
        // more fields...
    }

    // Here, instead of starting one's own boilerplate goroutine, just
    // hand responsibility over to the catacomb package. Evidently, it's
    // pretty hard to get this code wrong, so some might think it'd be ok
    // to write a panicky `MustInvoke(*Catacomb, func() error)`; please
    // don't do this in juju. (Anything that can go wrong will. Let's not
    // tempt fate.)
    err := catacomb.Invoke(catacomb.Plan{
        Site: &w.catacomb,
        Work: w.loop,
    })
    if err != nil {
        return nil, errors.Trace(err)
    }
    return w, nil
}

...with the standard Kill and Wait implementations just as expected:

func (w *someWorker) Kill() {
    w.catacomb.Kill(nil)
}

func (w *someWorker) Wait() error {
    return w.catacomb.Wait()
}

...and the ability for loop code to create workers and bind their lifetimes to the parent without risking the common misuse of a deferred watcher.Stop() that targets the parent's tomb -- which risks causing an initiating loop error to be overwritten by a later error from the Stop. Thus, while the Add in:

func (w *someWorker) loop() error {
    watch, err := w.config.Facade.WatchSomething()
    if err != nil {
        return errors.Annotate(err, "cannot watch something")
    }
    if err := w.catacomb.Add(watch); err != nil {
        // Note that Add takes responsibility for the supplied worker;
        // if the catacomb can't accept the worker (because it's already
        // dying) it will stop the worker and directly return any error
        // thus encountered.
        return errors.Trace(err)
    }

    for {
        select {
        case <-w.catacomb.Dying():
            // The other important difference is that there's no package-
            // level ErrDying -- it's just too risky. Catacombs supply
            // own ErrDying errors, and won't panic when they see them
            // coming from other catacombs.
            return w.catacomb.ErrDying()
        case change, ok := <-watch.Changes():
            if !ok {
                // Note: as discussed below, watcher.EnsureErr is an
                // antipattern. To actually write this code, we need to
                // (1) turn watchers into workers and (2) stop watchers
                // closing their channels on error.
                return errors.New("something watch failed")
            }
            if err := w.handle(change); err != nil {
                return nil, errors.Trace(err)
            }
        }
    }
}

...is not *obviously* superior to `defer watcher.Stop(watch, &w.tomb)`, it does in fact behave better; and, furthermore, is more amenable to future extension (watcher.Stop is fine *if* the watcher is started in NewWorker, and deferred to run *after* the tomb is killed with the loop error; but that becomes unwieldy when more than one watcher/worker is needed, and profoundly tedious when the set is either large or dynamic).

And that's not even getting into the issues with `watcher.EnsureErr`: this exists entirely because we picked a strange interface for watchers (Stop and Err, instead of Kill and Wait) that's not amenable to clean error-gathering; so we decided to signal worker errors with a closed change channel.

This solved the immediate problem, but caused us to add EnsureErr to make sure we still failed with *some* error if the watcher closed the chan without error: either because it broke its contract, or if some *other* component stopped the watcher cleanly. That is not ideal: it would be far better *never* to close. Then we can expect clients to Add the watch to a catacomb to handle lifetime, and they can expect the Changes channel to deliver changes alone.

Of course, client code still has to handle closed channels: once the scope of a chan gets beyond a single type, all users have to be properly paranoid, and e.g. expect channels to be closed even when the contract explicitly says they won't. But that's easy to track, and easy to handle -- just return an error complaining that the watcher broke its contract. Done.

It's also important to note that you can easily manage dynamic workers: once you've Add()ed the worker you can freely Kill() it at any time; so long as it cleans itself up successfully, and returns no error from Wait(), it will be silently unregistered and leave the catacomb otherwise unaffected. And that might happen in the loop goroutine; but it'll work just fine from anywhere.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Invoke

func Invoke(plan Plan) (err error)

Invoke uses the plan's catacomb to run the work func. It will return an error if the plan is not valid, or if the catacomb has already been used. If Invoke returns no error, the catacomb is now controlling the work func, and its exported methods can be called safely.

Invoke takes responsibility for all workers in plan.Init, *whether or not it succeeds*.

Types

type Catacomb

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

Catacomb is a variant of tomb.Tomb with its own internal goroutine, designed for coordinating the lifetimes of private workers needed by a single parent.

As a client, you should only ever create zero values; these should be used with Invoke to manage a parent task. No Catacomb methods are meaningful until the catacomb has been started with a successful Invoke.

See the package documentation for more detailed discussion and usage notes.

func (*Catacomb) Add

func (catacomb *Catacomb) Add(w worker.Worker) error

Add causes the supplied worker's lifetime to be bound to the catacomb's, relieving the client of responsibility for Kill()ing it and Wait()ing for an error, *whether or not this method succeeds*. If the method returns an error, it always indicates that the catacomb is shutting down; the value will either be the error from the (now-stopped) worker, or catacomb.ErrDying().

If the worker completes without error, the catacomb will continue unaffected; otherwise the catacomb's tomb will be killed with the returned error. This allows clients to freely Kill() workers that have been Add()ed; any errors encountered will still kill the catacomb, so the workers stay under control until the last moment, and so can be managed pretty casually once they've been added.

Don't try to add a worker to its own catacomb; that'll deadlock the shutdown procedure. I don't think there's much we can do about that.

func (*Catacomb) Context

func (catacomb *Catacomb) Context(parent context.Context) context.Context

Context returns a context that is a copy of the provided parent context with a replaced Done channel that is closed when either the catacomb is dying or the parent is cancelled.

If parent is nil, it defaults to the empty background context.

func (*Catacomb) Dead

func (catacomb *Catacomb) Dead() <-chan struct{}

Dead returns a channel that will be closed when Invoke has completed (and thus when subsequent calls to Wait() are known not to block).

func (*Catacomb) Dying

func (catacomb *Catacomb) Dying() <-chan struct{}

Dying returns a channel that will be closed when Kill is called.

func (*Catacomb) Err

func (catacomb *Catacomb) Err() error

Err returns the reason for the catacomb death provided via Kill or Killf, or ErrStillAlive when the catacomb is still alive.

func (*Catacomb) ErrDying

func (catacomb *Catacomb) ErrDying() error

ErrDying returns an error that can be used to Kill *this* catacomb without overwriting nil errors. It should only be used when the catacomb is already known to be dying; calling this method at any other time will return a different error, indicating client misuse.

func (*Catacomb) Kill

func (catacomb *Catacomb) Kill(err error)

Kill kills the Catacomb's internal tomb with the supplied error, or one derived from it.

  • if it's caused by this catacomb's ErrDying, it passes on tomb.ErrDying.
  • if it's tomb.ErrDying, or caused by another catacomb's ErrDying, it passes on a new error complaining about the misuse.
  • all other errors are passed on unmodified.

It's always safe to call Kill, but errors passed to Kill after the catacomb is dead will be ignored.

func (*Catacomb) Wait

func (catacomb *Catacomb) Wait() error

Wait blocks until Invoke completes, and returns the first non-nil and non-tomb.ErrDying error passed to Kill before Invoke finished.

type Plan

type Plan struct {

	// Site must point to an unused Catacomb.
	Site *Catacomb

	// Work will be run on a new goroutine, and tracked by Site.
	Work func() error

	// Init contains additional workers for which Site must be responsible.
	Init []worker.Worker
}

Plan defines the strategy for an Invoke.

func (Plan) Validate

func (plan Plan) Validate() error

Validate returns an error if the plan cannot be used. It doesn't check for reused catacombs: plan validity is necessary but not sufficient to determine that an Invoke will succeed.

Jump to

Keyboard shortcuts

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