github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/worker/catacomb/doc.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  /*
     5  Catacomb leverages tomb.Tomb to bind the lifetimes of, and track the errors
     6  of, a group of related workers. It's intended to be close to a drop-in
     7  replacement for a Tomb: if you're implementing a worker, the only differences
     8  should be (1) a slightly different creation dance; and (2) you can later call
     9  .Add(aWorker) to bind the worker's lifetime to the catacomb's, and cause errors
    10  from that worker to be exposed via the catacomb. Oh, and there's no global
    11  ErrDying to induce surprising panics when misused.
    12  
    13  This approach costs many extra goroutines over tomb.v2, but is slightly more
    14  robust because Catacomb.Add() verfies worker registration, and is thus safer
    15  than Tomb.Go(); and, of course, because it's designed to integrate with the
    16  worker.Worker model already common in juju.
    17  
    18  Note that a Catacomb is *not* a worker itself, despite the internal goroutine;
    19  it's a tool to help you construct workers, just like tomb.Tomb.
    20  
    21  The canonical expected construction of a catacomb-based worker is as follows:
    22  
    23      type someWorker struct {
    24          config   Config
    25          catacomb catacomb.Catacomb
    26          // more fields...
    27      }
    28  
    29      func NewWorker(config Config) (worker.Worker, error) {
    30  
    31          // This chunk is exactly as you'd expect for a tomb worker: just
    32          // create the instance with an implicit zero catacomb.
    33          if err := config.Validate(); err != nil {
    34              return nil, errors.Trace(err)
    35          }
    36          w := &someWorker{
    37              config:   config,
    38              // more fields...
    39          }
    40  
    41          // Here, instead of starting one's own boilerplate goroutine, just
    42          // hand responsibility over to the catacomb package. Evidently, it's
    43          // pretty hard to get this code wrong, so some might think it'd be ok
    44          // to write a panicky `MustInvoke(*Catacomb, func() error)`; please
    45          // don't do this in juju. (Anything that can go wrong will. Let's not
    46          // tempt fate.)
    47          err := catacomb.Invoke(catacomb.Plan{
    48              Site: &w.catacomb,
    49              Work: w.loop,
    50          })
    51          if err != nil {
    52              return nil, errors.Trace(err)
    53          }
    54          return w, nil
    55      }
    56  
    57  ...with the standard Kill and Wait implementations just as expected:
    58  
    59      func (w *someWorker) Kill() {
    60          w.catacomb.Kill(nil)
    61      }
    62  
    63      func (w *someWorker) Wait() error {
    64          return w.catacomb.Wait()
    65      }
    66  
    67  ...and the ability for loop code to create workers and bind their lifetimes
    68  to the parent without risking the common misuse of a deferred watcher.Stop()
    69  that targets the parent's tomb -- which risks causing an initiating loop error
    70  to be overwritten by a later error from the Stop. Thus, while the Add in:
    71  
    72      func (w *someWorker) loop() error {
    73          watch, err := w.config.Facade.WatchSomething()
    74          if err != nil {
    75              return errors.Annotate(err, "cannot watch something")
    76          }
    77          if err := w.catacomb.Add(watch); err != nil {
    78              // Note that Add takes responsibility for the supplied worker;
    79              // if the catacomb can't accept the worker (because it's already
    80              // dying) it will stop the worker and directly return any error
    81              // thus encountered.
    82              return errors.Trace(err)
    83          }
    84  
    85          for {
    86              select {
    87              case <-w.catacomb.Dying():
    88                  // The other important difference is that there's no package-
    89                  // level ErrDying -- it's just too risky. Catacombs supply
    90                  // own ErrDying errors, and won't panic when they see them
    91                  // coming from other catacombs.
    92                  return w.catacomb.ErrDying()
    93              case change, ok := <-watch.Changes():
    94                  if !ok {
    95                      // Note: as discussed below, watcher.EnsureErr is an
    96                      // antipattern. To actually write this code, we need to
    97                      // (1) turn watchers into workers and (2) stop watchers
    98                      // closing their channels on error.
    99                      return errors.New("something watch failed")
   100                  }
   101                  if err := w.handle(change); err != nil {
   102                      return nil, errors.Trace(err)
   103                  }
   104              }
   105          }
   106      }
   107  
   108  ...is not *obviously* superior to `defer watcher.Stop(watch, &w.tomb)`, it
   109  does in fact behave better; and, furthermore, is more amenable to future
   110  extension (watcher.Stop is fine *if* the watcher is started in NewWorker,
   111  and deferred to run *after* the tomb is killed with the loop error; but that
   112  becomes unwieldy when more than one watcher/worker is needed, and profoundly
   113  tedious when the set is either large or dynamic).
   114  
   115  And that's not even getting into the issues with `watcher.EnsureErr`: this
   116  exists entirely because we picked a strange interface for watchers (Stop and
   117  Err, instead of Kill and Wait) that's not amenable to clean error-gathering;
   118  so we decided to signal worker errors with a closed change channel.
   119  
   120  This solved the immediate problem, but caused us to add EnsureErr to make sure
   121  we still failed with *some* error if the watcher closed the chan without error:
   122  either because it broke its contract, or if some *other* component stopped the
   123  watcher cleanly. That is not ideal: it would be far better *never* to close.
   124  Then we can expect clients to Add the watch to a catacomb to handle lifetime,
   125  and they can expect the Changes channel to deliver changes alone.
   126  
   127  Of course, client code still has to handle closed channels: once the scope of
   128  a chan gets beyond a single type, all users have to be properly paranoid, and
   129  e.g. expect channels to be closed even when the contract explicitly says they
   130  won't. But that's easy to track, and easy to handle -- just return an error
   131  complaining that the watcher broke its contract. Done.
   132  
   133  It's also important to note that you can easily manage dynamic workers: once
   134  you've Add()ed the worker you can freely Kill() it at any time; so long as it
   135  cleans itself up successfully, and returns no error from Wait(), it will be
   136  silently unregistered and leave the catacomb otherwise unaffected. And that
   137  might happen in the loop goroutine; but it'll work just fine from anywhere.
   138  */
   139  package catacomb