~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/worker/dependency/doc.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
// Copyright 2015 Canonical Ltd.
 
2
// Licensed under the AGPLv3, see LICENCE file for details.
 
3
 
 
4
/*
 
5
 
 
6
The dependency package exists to address a general problem with shared resources
 
7
and the management of their lifetimes. Many kinds of software handle these issues
 
8
with more or less felicity, but it's particularly important that juju (which is
 
9
a distributed system that needs to be very fault-tolerant) handle them clearly
 
10
and sanely.
 
11
 
 
12
Background
 
13
----------
 
14
 
 
15
A cursory examination of the various workers run in juju agents (as of 2015-04-20)
 
16
reveals a distressing range of approaches to the shared resource problem. A
 
17
sampling of techniques (and their various problems) follows:
 
18
 
 
19
  * enforce sharing in code structure, either directly via scoping or implicitly
 
20
    via nested runners (state/api conns; agent config)
 
21
      * code structure is inflexible, and it enforces strictly nested resource
 
22
        lifetimes, which are not always adequate.
 
23
  * just create N of them and hope it works out OK (environs)
 
24
      * creating N prevents us from, e.g., using a single connection to an environ
 
25
        and sanely rate-limiting ourselves.
 
26
  * use filesystem locking across processes (machine execution lock)
 
27
      * implementation sometimes flakes out, or is used improperly; and multiple
 
28
        agents *are* a problem anyway, but even if we're all in-process we'll need
 
29
        some shared machine lock...
 
30
  * wrap workers to start up only when some condition is met (post-upgrade
 
31
    stability -- itself also a shared resource)
 
32
      * lifetime-nesting comments apply here again; *and* it makes it harder to
 
33
        follow the code.
 
34
  * implement a singleton (lease manager)
 
35
      * singletons make it *even harder* to figure out what's going on -- they're
 
36
        basically just fancy globals, and have all the associated problems with,
 
37
        e.g. deadlocking due to unexpected shutdown order.
 
38
 
 
39
...but, of course, they all have their various advantages:
 
40
 
 
41
  * Of the approaches, the first is the most reliable by far. Despite the
 
42
    inflexibility, there's a clear and comprehensible model in play that has yet
 
43
    to cause serious confusion: each worker is created with its resource(s)
 
44
    directly available in code scope, and trusts that it will be restarted by an
 
45
    independent watchdog if one of its dependencies fails. This characteristic is
 
46
    extremely beneficial and must be preserved; we just need it to be more
 
47
    generally applicable.
 
48
 
 
49
  * The create-N-Environs approach is valuable because it can be simply (if
 
50
    inelegantly) integrated with its dependent worker, and a changed Environ
 
51
    does not cause the whole dependent to fall over (unless the change is itself
 
52
    bad). The former characteristic is a subtle trap (we shouldn't be baking
 
53
    dependency-management complexity into the cores of our workers' select loops,
 
54
    even if it is "simple" to do so), but the latter is important: in particular,
 
55
    firewaller and provisioner are distressingly heavyweight workers and it would
 
56
    be unwise to take an approach that led to them being restarted when not
 
57
    necessary.
 
58
 
 
59
  * The filesystem locking just should not happen -- and we need to integrate the
 
60
    unit and machine agents to eliminate it (and for other reasons too) so we
 
61
    should give some thought to the fact that we'll be shuffling these dependencies
 
62
    around pretty hard in the future. If the approach can make that task easier,
 
63
    then great.
 
64
 
 
65
  * The singleton is dangerous specifically because its dependency interactions are
 
66
    unclear. Absolute clarity of dependencies, as provided by the nesting approaches,
 
67
    is in fact critical; but the sheer convenience of the singleton is alluring, and
 
68
    reminds us that the approach we take must remain easy to use.
 
69
 
 
70
The various nesting approaches give easy access to directly-available resources,
 
71
which is great, but will fail as soon as you have a sufficiently sophisticated
 
72
dependent that can operate usefully without all its dependencies being satisfied
 
73
(we have a couple of requirements for this in the unit agent right now). Still,
 
74
direct resource access *is* tremendously convenient, and we need some way to
 
75
access one service from another.
 
76
 
 
77
However, all of these resources are very different: for a solution that encompasses
 
78
them all, you kinda have to represent them as interface{} at some point, and that's
 
79
very risky re: clarity.
 
80
 
 
81
 
 
82
Problem
 
83
-------
 
84
 
 
85
The package is intended to implement the following developer stories:
 
86
 
 
87
  * As a developer trying to understand the codebase, I want to know what workers
 
88
    are running in an agent at any given time.
 
89
  * As a developer, I want to be prevented from introducing dependency cycles
 
90
    into my application.
 
91
  * As a developer, I want to provide a service provided by some worker to one or
 
92
    more client workers.
 
93
  * As a developer, I want to write a service that consumes one or more other
 
94
    workers' services.
 
95
  * As a developer, I want to choose how I respond to missing dependencies.
 
96
  * As a developer, I want to be able to inject test doubles for my dependencies.
 
97
  * As a developer, I want control over how my service is exposed to others.
 
98
  * As a developer, I don't want to have to typecast my dependencies from
 
99
    interface{} myself.
 
100
  * As a developer, I want my service to be restarted if its dependencies change.
 
101
 
 
102
That last one might bear a little bit of explanation: but I contend that it's the
 
103
only reliable approach to writing resilient services that compose sanely into a
 
104
comprehensible system. Consider:
 
105
 
 
106
  * Juju agents' lifetimes must be assumed to exceed the MTBR of the systems
 
107
    they're deployed on; you might naively think that hard reboots are "rare"...
 
108
    but they're not. They really are just a feature of the terrain we have to
 
109
    traverse. Therefore every worker *always* has to be capable of picking itself
 
110
    back up from scratch and continuing sanely. That is, we're not imposing a new
 
111
    expectation: we're just working within the existing constraints.
 
112
  * While some workers are simple, some are decidedly not; when a worker has any
 
113
    more complexity than "none" it is a Bad Idea to mix dependency-management
 
114
    concerns into their core logic: it creates the sort of morass in which subtle
 
115
    bugs thrive.
 
116
 
 
117
So, we take advantage of the expected bounce-resilience, and excise all dependency
 
118
management concerns from the existing ones... in favour of a system that bounces
 
119
workers slightly more often than before, and thus exercises those code paths more;
 
120
so, when there are bugs, we're more likely to shake them out in automated testing
 
121
before they hit users.
 
122
 
 
123
We'd maybe also like to implement this story:
 
124
 
 
125
  * As a developer, I want to add and remove groups of workers atomically, e.g.
 
126
    when starting the set of controller workers for a hosted environ; or when
 
127
    starting the set of workers used by a single unit. [NOT DONE]
 
128
 
 
129
...but there's no urgent use case yet, and it's not certain to be superior to an
 
130
engine-nesting approach.
 
131
 
 
132
 
 
133
Solution
 
134
--------
 
135
 
 
136
Run a single dependency.Engine at the top level of each agent; express every
 
137
shared resource, and every worker that uses one, as a dependency.Manifold; and
 
138
install them all into the top-level engine.
 
139
 
 
140
When installed under some name, a dependency.Manifold represents the features of
 
141
a node in the engine's dependency graph. It lists:
 
142
 
 
143
  * The names of its dependencies (Inputs).
 
144
  * How to create the worker representing the resource (Start).
 
145
  * How (if at all) to expose the resource as a service to other resources that
 
146
    know it by name (Output).
 
147
 
 
148
...and allows the developers of each independent service a common mechanism for
 
149
declaring and accessing their dependencies, and the ability to assume that they
 
150
will be restarted whenever there is a material change to their accessible
 
151
dependencies.
 
152
 
 
153
When the weight of manifolds in a single engine becomes inconvenient, group them
 
154
and run them inside nested dependency.Engines; the Report() method on the top-
 
155
level engine will collect information from (directly-) contained engines, so at
 
156
least there's still some observability; but there may also be call to pass
 
157
actual dependencies down from one engine to another, and that'll demand careful
 
158
thought.
 
159
 
 
160
 
 
161
Usage
 
162
-----
 
163
 
 
164
In each worker package, write a `manifold.go` containing the following:
 
165
 
 
166
    // ManifoldConfig holds the information necessary to configure the worker
 
167
    // controlled by a Manifold.
 
168
    type ManifoldConfig struct {
 
169
 
 
170
        // The names of the various dependencies, e.g.
 
171
        APICallerName   string
 
172
 
 
173
        // Any other required top-level configuration, e.g.
 
174
        Period time.Duration
 
175
    }
 
176
 
 
177
    // Manifold returns a manifold that controls the operation of a worker
 
178
    // responsible for <things>, configured as supplied.
 
179
    func Manifold(config ManifoldConfig) dependency.Manifold {
 
180
        // Your code here...
 
181
        return dependency.Manifold{
 
182
 
 
183
            // * certainly include each of your configured dependency names,
 
184
            //   getResource will only expose them if you declare them here.
 
185
            Inputs: []string{config.APICallerName, config.MachineLockName},
 
186
 
 
187
            // * certainly include a start func, it will panic if you don't.
 
188
            Start: func(getResource dependency.GetResourceFunc) (worker.Worker, error) {
 
189
                // You presumably want to get your dependencies, and you almost
 
190
                // certainly want to be closed over `config`...
 
191
                var apicaller base.APICaller
 
192
                if err := getResource(config.APICallerName, &apicaller); err != nil {
 
193
                    return nil, err
 
194
                }
 
195
                return newSomethingWorker(apicaller, config.Period)
 
196
            },
 
197
 
 
198
            // * output func is not obligatory, and should be skipped if you
 
199
            //   don't know what you'll be exposing or to whom.
 
200
            // * see `worker/gate`, `worker/util`, and
 
201
            //   `worker/dependency/testing` for examples of output funcs.
 
202
            // * if you do supply an output func, be sure to document it on the
 
203
            //   Manifold func; for example:
 
204
            //
 
205
            //       // Manifold exposes Foo and Bar resources, which can be
 
206
            //       // accessed by passing a *Foo or a *Bar in the output
 
207
            //       // parameter of its dependencies' getResouce calls.
 
208
            Output: nil,
 
209
        }
 
210
    }
 
211
 
 
212
...and take care to construct your manifolds *only* via that function; *all*
 
213
your dependencies *must* be declared in your ManifoldConfig, and *must* be
 
214
accessed via those names. Don't hardcode anything, please.
 
215
 
 
216
If you find yourself using the same manifold configuration in several places,
 
217
consider adding helpers to cmd/jujud/agent/engine, which includes mechanisms
 
218
for simple definition of manifolds that depend on an API caller; on an agent;
 
219
or on both.
 
220
 
 
221
 
 
222
Testing
 
223
-------
 
224
 
 
225
The `worker/dependency/testing` package, commonly imported as "dt", exposes a
 
226
`StubResource` that is helpful for testing `Start` funcs in decent isolation,
 
227
with mocked dependencies. Tests for `Inputs` and `Output` are generally pretty
 
228
specific to their precise context and don't seem to benefit much from
 
229
generalisation.
 
230
 
 
231
 
 
232
Special considerations
 
233
----------------------
 
234
 
 
235
The nodes in your *dependency* graph must be acyclic; this does not imply that
 
236
the *information flow* must be acyclic. Indeed, it is common for separate
 
237
components to need to synchronise their actions; but the implementation of
 
238
Engine makes it inconvenient for either one to depend on the other (and
 
239
impossible for both to do so).
 
240
 
 
241
When a set of manifolds need to encode a set of services whose information flow
 
242
is not acyclic, apparent A->B->A cycles can be broken by introducing a new
 
243
shared dependency C to mediate the information flow. That is, A and B can then
 
244
separately depend upon C; and C itself can start a degenerate worker that never
 
245
errors of its own accord.
 
246
 
 
247
For examples of this technique, search for `cmd/jujud/agent/engine.NewValueWorker`
 
248
(which is generally used inside other manifolds to pass snippets of agent config
 
249
down to workers that don't have a good reason to see, or write, the full agent
 
250
config); and `worker/gate.Manifold`, which is for one-way coordination between
 
251
workers which should not be started until some other worker has completed some
 
252
task.
 
253
 
 
254
Please be careful when coordinating workers like this; the gate manifold in
 
255
particular is effectively just another lock, and it'd be trivial to construct
 
256
a set of gate-users that can deadlock one another. All the usual considerations
 
257
when working with locks still apply.
 
258
 
 
259
 
 
260
Concerns and mitigations thereof
 
261
--------------------------------
 
262
 
 
263
The dependency package will *not* provide the following features:
 
264
 
 
265
  * Deterministic worker startup. As above, this is a blessing in disguise: if
 
266
    your workers have a problem with this, they're using magical undeclared
 
267
    dependencies and we get to see the inevitable bugs sooner.
 
268
    TODO(fwereade): we should add fuzz to the bounce and restart durations to
 
269
    more vigorously shake out the bugs...
 
270
  * Hand-holding for developers writing Output funcs; the onus is on you to
 
271
    document what you expose; produce useful error messages when they supplied
 
272
    with unexpected types via the interface{} param; and NOT to panic. The onus
 
273
    on your clients is only to read your docs and handle the errors you might
 
274
    emit.
 
275
 
 
276
*/
 
277
package dependency