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