-
Notifications
You must be signed in to change notification settings - Fork 0
/
hacking-state.txt
153 lines (124 loc) · 6.95 KB
/
hacking-state.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
Hacking the juju-core/state package
===================================
This document remains a work in progress; it's an attempt to capture
the various conventions and things to bear in mind that aren't
necessarily written down anywhere else.
return values: ok vs err
------------------------
By convention, anything that could reasonably fail must use a separate
channel to communicate the failure. Broadly speaking, methods that can
fail in more than one way must return an error; those that can only
fail in one way (eg Machine.InstanceId: the value is either valid or
missing) are expected to return a bool for consistency's sake, even if
the type of the return value is such that failure can be signalled in-
band.
changes to entities
-------------------
Entity objects reflect remote state that may change at any time, but we
don't want to make that too obvious. By convention, the only methods
that should change an entity's in-memory document are as follows:
* Refresh(), which should update every field.
* Methods that set fields remotely should update only those fields
locally.
The upshot of this is that it's not appropriate to call Refresh on any
argument to a state method, including receivers; if you're in a context
in which that would be helpful, you should clone the entity first. This
is simple enough that there's no Clone() method, but it would be kinda
nice to implement them in our Copious Free Time; I think there are
places outside state that would also find it useful.
care and feeding of mgo/txn
---------------------------
Just about all our writes to mongodb are mediated by the mgo/txn
package, and using this correctly demands some care. Not all the code
has been written in a fully aware state, and cases in which existing
practice is divergent from the advice given below should be regarded
with some suspicion.
The txn package lets you make watchable changes to mongodb via lists of
operations with the following critical properties:
* transactions can apply to more than one document (this is rather
the point of having them).
* transactions will complete if every assert in the transaction
passes; they will not run at all if any assert fails.
* multi-document transactions are *not* atomic; the operations are
applied in the order specified by the list.
* operations, and hence assertions, can only be applied to documents
with ids that are known at the time the operation list is built;
this means that it takes extra work to specify a condition like
"no unit document newer than X exists".
The second point above deserves further discussion. Whenever you are
implementing a state change, you should consider the impact of the
following possible mongodb states on your code:
* if mongodb is already in a state consistent with the transaction
having already been run -- which is *always* possible -- you
should return immediately without error.
* if mongodb is in a state that indicates the transaction is not
valid -- eg trying to add a unit to a dying service -- you should
return immediately with a descriptive error.
Each of the above situations should generally be checked as part of
preparing a new []txn.Op, but in some cases it's convenient to trust
(to begin with) that the entity's in-memory state reflects reality.
Regardless, your job is to build a list of operations which assert
that:
* the transaction still needs to be applied.
* the transaction is actively valid.
* facts on which the transaction list's form depends remain true.
If you're really lucky you'll get to write a transaction in which
the third requirement collapses to nothing, but that's not really
the norm. In practice, you need to be prepared for the run to return
txn.ErrAborted; if this happens, you need to check for previous success
(and return nil) or for known cause of invalidity (and return an error
describing it).
If neither of these cases apply, you should assume it's an assertion
failure of the third kind; in that case, you should build a new
transaction based on more recent state and try again. If ErrAborteds
just keep coming, give up; there's an ErrExcessiveContention that
helps to describe the situation.
watching entities, and select groups thereof
--------------------------------------------
The mgo/txn log enables very convenient notifications of changes to
particular documents and groups thereof. The state/watcher package
converts the txn event log into events and send them down client-
supplied channels for further processing; the state code itself
implements watchers in terms of these events.
All the internally-relevant watching code is implemented in the file
state/watcher.go. These constructs can be broadly divided into groups
as follows:
* single-document watchers: dead simple, they notify of every
change to a given doc. SettingsWatcher bucks the convention of
NotifyWatcher in that it reads whole *Settings~s to send down the
channel rather than just sending notifications (we probably
shouldn't do that, but I don't think there's time to fix it right
now).
* entity group watchers: pretty simple, very common; they notify of
every change to the Life field among a group of entities in the
same collection, with group membership determined in a wide variety
of ways.
* relation watchers: of a similar nature to entity group watchers,
but generating carefully-ordered events from observed changes to
several different collections, none of which have Life fields.
Implementation of new watchers is not necessarily a simple task, unless
it's a genuinely trivial refactoring (or, given lack of generics, copy-
and-paste job). Unless you already know exactly what you're doing,
please start a discussion about what you're trying to do before you
embark on further work in this area.
transactions and reference counts
---------------------------------
As described above, it's difficult to assert things like "no units of
service X exist". It can be done, but not without additional work, and
we're generally using reference counts to enable this sort of thing.
In general, reference counts should not be stored within entities: such
changes are part of the operation of juju and are not important enough
to be reflected as a constant stream of events. We didn't figure this
out until recently, though, so relationDoc has a UnitCount, and
serviceDoc has both a UnitCount and a RelationCount. This doesn't
matter for the relation -- nothing watches relation docs directly --
but I fear it matters quite hard for service, because every added or
removed unit or relation will be an unnecessary event sent to every
unit agent.
We'll deal with that when it's a problem rather than just a concern;
but new reference counts should not be thoughtlessly added to entity
documents, and opportunities to separate them from existing docs should
be considered seriously.
[TODO: write about globalKey and what it's good for... constraints, settings,
statuses, etc; occasionaly profitable use of fast prefix lookup on indexed
fields.]