/
resolver.go
396 lines (351 loc) · 12.7 KB
/
resolver.go
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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
// Copyright 2020 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package relation
import (
"github.com/juju/charm/v12/hooks"
"github.com/juju/collections/set"
"github.com/juju/errors"
"github.com/juju/names/v5"
"github.com/kr/pretty"
"github.com/juju/juju/core/life"
"github.com/juju/juju/worker/uniter/hook"
"github.com/juju/juju/worker/uniter/operation"
"github.com/juju/juju/worker/uniter/remotestate"
"github.com/juju/juju/worker/uniter/resolver"
)
// Logger is here to stop the desire of creating a package level Logger.
// Don't do this, instead use the one passed into the new resolver function.
type logger interface{}
var _ logger = struct{}{}
// Logger represents the logging methods used in this package.
type Logger interface {
Errorf(string, ...interface{})
Warningf(string, ...interface{})
Infof(string, ...interface{})
Debugf(string, ...interface{})
Tracef(string, ...interface{})
IsTraceEnabled() bool
}
// NewRelationResolver returns a resolver that handles all relation-related
// hooks (except relation-created) and is wired to the provided RelationStateTracker
// instance.
func NewRelationResolver(stateTracker RelationStateTracker, subordinateDestroyer SubordinateDestroyer, logger Logger) resolver.Resolver {
return &relationsResolver{
stateTracker: stateTracker,
subordinateDestroyer: subordinateDestroyer,
logger: logger,
}
}
type relationsResolver struct {
stateTracker RelationStateTracker
subordinateDestroyer SubordinateDestroyer
logger Logger
}
// NextOp implements resolver.Resolver.
func (r *relationsResolver) NextOp(localState resolver.LocalState, remoteState remotestate.Snapshot, opFactory operation.Factory) (_ operation.Operation, err error) {
if r.logger.IsTraceEnabled() {
r.logger.Tracef("relation resolver next op for new remote relations %# v", pretty.Formatter(remoteState.Relations))
defer func() {
if err == resolver.ErrNoOperation {
r.logger.Tracef("no relation operation to run")
}
}()
}
if err := r.maybeDestroySubordinates(remoteState); err != nil {
return nil, errors.Trace(err)
}
if localState.Kind != operation.Continue {
return nil, resolver.ErrNoOperation
}
if err := r.stateTracker.SynchronizeScopes(remoteState); err != nil {
return nil, errors.Trace(err)
}
// Check whether we need to fire a hook for any of the relations
for relationId, relationSnapshot := range remoteState.Relations {
if !r.stateTracker.IsKnown(relationId) {
r.logger.Tracef("unknown relation %d resolving next op", relationId)
continue
} else if isImplicit, _ := r.stateTracker.IsImplicit(relationId); isImplicit {
continue
}
// If either the unit or the relation are Dying, or the
// relation becomes suspended, then the relation should be
// broken.
var remoteBroken bool
if remoteState.Life == life.Dying || relationSnapshot.Life == life.Dying || relationSnapshot.Suspended {
relationSnapshot = remotestate.RelationSnapshot{}
remoteBroken = true
// TODO(axw) if relation is implicit, leave scope & remove.
}
// Examine local/remote states and figure out if a hook needs
// to be fired for this relation.
relState, err := r.stateTracker.State(relationId)
if err != nil {
//
relState = NewState(relationId)
}
hInfo, err := r.nextHookForRelation(relState, relationSnapshot, remoteBroken)
if err == resolver.ErrNoOperation {
continue
}
return opFactory.NewRunHook(hInfo)
}
return nil, resolver.ErrNoOperation
}
// maybeDestroySubordinates checks whether the remote state indicates that the
// unit is dying and ensures that any related subordinates are properly
// destroyed.
func (r *relationsResolver) maybeDestroySubordinates(remoteState remotestate.Snapshot) error {
if remoteState.Life != life.Dying {
return nil
}
var destroyAllSubordinates bool
for relationId, relationSnapshot := range remoteState.Relations {
if relationSnapshot.Life != life.Alive {
continue
} else if hasContainerScope, err := r.stateTracker.HasContainerScope(relationId); err != nil || !hasContainerScope {
continue
}
// Found alive relation to a subordinate
relationSnapshot.Life = life.Dying
remoteState.Relations[relationId] = relationSnapshot
destroyAllSubordinates = true
}
if destroyAllSubordinates {
return r.subordinateDestroyer.DestroyAllSubordinates()
}
return nil
}
func (r *relationsResolver) nextHookForRelation(localState *State, remote remotestate.RelationSnapshot, remoteBroken bool) (hook.Info, error) {
// If there's a guaranteed next hook, return that.
relationId := localState.RelationId
if localState.ChangedPending != "" {
// ChangedPending should only happen for a unit (not an app). It is a side effect that if we call 'relation-joined'
// for a unit, we immediately queue up relation-changed for that unit, before we run any other hooks
// Applications never see "relation-joined".
unitName := localState.ChangedPending
appName, err := names.UnitApplication(unitName)
if err != nil {
return hook.Info{}, errors.Annotate(err, "changed pending held an invalid unit name")
}
return hook.Info{
Kind: hooks.RelationChanged,
RelationId: relationId,
RemoteUnit: unitName,
RemoteApplication: appName,
ChangeVersion: remote.Members[unitName],
}, nil
}
// Get related app names, trigger all app hooks first
allAppNames := set.NewStrings()
for appName := range localState.ApplicationMembers {
allAppNames.Add(appName)
}
for app := range remote.ApplicationMembers {
allAppNames.Add(app)
}
sortedAppNames := allAppNames.SortedValues()
// Get the union of all relevant units, and sort them, so we produce events
// in a consistent order (largely for the convenience of the tests).
allUnitNames := set.NewStrings()
for unitName := range localState.Members {
allUnitNames.Add(unitName)
}
for unitName := range remote.Members {
allUnitNames.Add(unitName)
}
sortedUnitNames := allUnitNames.SortedValues()
if allUnitNames.Contains("") {
return hook.Info{}, errors.Errorf("somehow we got the empty unit. localState: %v, remote: %v", localState.Members, remote.Members)
}
// If there are any locally known units that are no longer reflected in
// remote state, depart them.
for _, unitName := range sortedUnitNames {
changeVersion, found := localState.Members[unitName]
if !found {
continue
}
if _, found := remote.Members[unitName]; !found {
appName, err := names.UnitApplication(unitName)
if err != nil {
return hook.Info{}, errors.Trace(err)
}
// Consult the life of the localState unit and/or app to
// figure out if its the localState or the remote unit going
// away. Note that if the app is removed, the unit will
// still be alive but its parent app will by dying.
localUnitLife, localAppLife, err := r.stateTracker.LocalUnitAndApplicationLife()
if err != nil {
return hook.Info{}, errors.Trace(err)
}
var departee = unitName
if localUnitLife != life.Alive || localAppLife != life.Alive {
departee = r.stateTracker.LocalUnitName()
}
return hook.Info{
Kind: hooks.RelationDeparted,
RelationId: relationId,
RemoteUnit: unitName,
RemoteApplication: appName,
ChangeVersion: changeVersion,
DepartingUnit: departee,
}, nil
}
}
// If the relation's meant to be broken, break it. A side-effect of
// the logic that generates the relation-created hooks is that we may
// end up in this block for a peer relation. Since you cannot depart
// peer relations we can safely ignore this hook.
isPeer, _ := r.stateTracker.IsPeerRelation(relationId)
if remoteBroken && !isPeer {
if !r.stateTracker.StateFound(relationId) {
// The relation may have been suspended and then
// removed, so we don't want to run the hook twice.
return hook.Info{}, resolver.ErrNoOperation
}
return hook.Info{
Kind: hooks.RelationBroken,
RelationId: relationId,
RemoteApplication: r.stateTracker.RemoteApplication(relationId),
}, nil
}
for _, appName := range sortedAppNames {
changeVersion, found := remote.ApplicationMembers[appName]
if !found {
// ?
continue
}
// Note(jam): 2019-10-23 For compatibility purposes, we don't trigger a hook if
// localState.ApplicationMembers doesn't contain the app and the changeVersion == 0.
// This is because otherwise all charms always get a hook with the app
// as the context, and that is likely to expose them to something they
// may not be ready for. Also, since no app content has been set, there
// is nothing for them to respond to.
if oldVersion := localState.ApplicationMembers[appName]; oldVersion != changeVersion {
return hook.Info{
Kind: hooks.RelationChanged,
RelationId: relationId,
RemoteUnit: "",
RemoteApplication: appName,
ChangeVersion: changeVersion,
}, nil
}
}
// If there are any remote units not locally known, join them.
for _, unitName := range sortedUnitNames {
changeVersion, found := remote.Members[unitName]
if !found {
r.logger.Tracef("cannot join relation %d, no known Members for %q", relationId, unitName)
continue
}
if _, found := localState.Members[unitName]; !found {
appName, err := names.UnitApplication(unitName)
if err != nil {
return hook.Info{}, errors.Trace(err)
}
return hook.Info{
Kind: hooks.RelationJoined,
RelationId: relationId,
RemoteUnit: unitName,
RemoteApplication: appName,
ChangeVersion: changeVersion,
}, nil
} else {
r.logger.Debugf("unit %q already joined relation %d", unitName, relationId)
}
}
// Finally scan for remote units whose latest version is not reflected
// in localState state.
for _, unitName := range sortedUnitNames {
remoteChangeVersion, found := remote.Members[unitName]
if !found {
continue
}
localChangeVersion, found := localState.Members[unitName]
if !found {
continue
}
appName, err := names.UnitApplication(unitName)
if err != nil {
return hook.Info{}, errors.Trace(err)
}
// NOTE(axw) we use != and not > to cater due to the
// use of the relation settings document's txn-revno
// as the version. When model-uuid migration occurs, the
// document is recreated, resetting txn-revno.
if remoteChangeVersion != localChangeVersion {
return hook.Info{
Kind: hooks.RelationChanged,
RelationId: relationId,
RemoteUnit: unitName,
RemoteApplication: appName,
ChangeVersion: remoteChangeVersion,
}, nil
}
}
// Nothing left to do for this relation.
return hook.Info{}, resolver.ErrNoOperation
}
// NewCreatedRelationResolver returns a resolver that handles relation-created
// hooks and is wired to the provided RelationStateTracker instance.
func NewCreatedRelationResolver(stateTracker RelationStateTracker, logger Logger) resolver.Resolver {
return &createdRelationsResolver{
stateTracker: stateTracker,
logger: logger,
}
}
type createdRelationsResolver struct {
stateTracker RelationStateTracker
logger Logger
}
// NextOp implements resolver.Resolver.
func (r *createdRelationsResolver) NextOp(
localState resolver.LocalState,
remoteState remotestate.Snapshot,
opFactory operation.Factory,
) (_ operation.Operation, err error) {
if r.logger.IsTraceEnabled() {
r.logger.Tracef("create relation resolver next op for new remote relations %# v", pretty.Formatter(remoteState.Relations))
defer func() {
if err == resolver.ErrNoOperation {
r.logger.Tracef("no create relation operation to run")
}
}()
}
// Nothing to do if not yet installed or if the unit is dying.
if !localState.Installed || remoteState.Life == life.Dying {
return nil, resolver.ErrNoOperation
}
// We should only evaluate the resolver logic if there is no other pending operation
if localState.Kind != operation.Continue {
return nil, resolver.ErrNoOperation
}
if err := r.stateTracker.SynchronizeScopes(remoteState); err != nil {
return nil, errors.Trace(err)
}
for relationId, relationSnapshot := range remoteState.Relations {
if relationSnapshot.Life != life.Alive {
continue
}
hook, err := r.nextHookForRelation(relationId)
if err != nil {
if err == resolver.ErrNoOperation {
continue
}
return nil, errors.Trace(err)
}
return opFactory.NewRunHook(hook)
}
return nil, resolver.ErrNoOperation
}
func (r *createdRelationsResolver) nextHookForRelation(relationId int) (hook.Info, error) {
isImplicit, _ := r.stateTracker.IsImplicit(relationId)
if r.stateTracker.RelationCreated(relationId) || isImplicit {
return hook.Info{}, resolver.ErrNoOperation
}
return hook.Info{
Kind: hooks.RelationCreated,
RelationId: relationId,
RemoteApplication: r.stateTracker.RemoteApplication(relationId),
}, nil
}