github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/worker/uniter/relation/resolver.go (about) 1 // Copyright 2020 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package relation 5 6 import ( 7 "github.com/juju/charm/v12/hooks" 8 "github.com/juju/collections/set" 9 "github.com/juju/errors" 10 "github.com/juju/names/v5" 11 "github.com/kr/pretty" 12 13 "github.com/juju/juju/core/life" 14 "github.com/juju/juju/worker/uniter/hook" 15 "github.com/juju/juju/worker/uniter/operation" 16 "github.com/juju/juju/worker/uniter/remotestate" 17 "github.com/juju/juju/worker/uniter/resolver" 18 ) 19 20 // Logger is here to stop the desire of creating a package level Logger. 21 // Don't do this, instead use the one passed into the new resolver function. 22 type logger interface{} 23 24 var _ logger = struct{}{} 25 26 // Logger represents the logging methods used in this package. 27 type Logger interface { 28 Errorf(string, ...interface{}) 29 Warningf(string, ...interface{}) 30 Infof(string, ...interface{}) 31 Debugf(string, ...interface{}) 32 Tracef(string, ...interface{}) 33 IsTraceEnabled() bool 34 } 35 36 // NewRelationResolver returns a resolver that handles all relation-related 37 // hooks (except relation-created) and is wired to the provided RelationStateTracker 38 // instance. 39 func NewRelationResolver(stateTracker RelationStateTracker, subordinateDestroyer SubordinateDestroyer, logger Logger) resolver.Resolver { 40 return &relationsResolver{ 41 stateTracker: stateTracker, 42 subordinateDestroyer: subordinateDestroyer, 43 logger: logger, 44 } 45 } 46 47 type relationsResolver struct { 48 stateTracker RelationStateTracker 49 subordinateDestroyer SubordinateDestroyer 50 logger Logger 51 } 52 53 // NextOp implements resolver.Resolver. 54 func (r *relationsResolver) NextOp(localState resolver.LocalState, remoteState remotestate.Snapshot, opFactory operation.Factory) (_ operation.Operation, err error) { 55 if r.logger.IsTraceEnabled() { 56 r.logger.Tracef("relation resolver next op for new remote relations %# v", pretty.Formatter(remoteState.Relations)) 57 defer func() { 58 if err == resolver.ErrNoOperation { 59 r.logger.Tracef("no relation operation to run") 60 } 61 }() 62 } 63 if err := r.maybeDestroySubordinates(remoteState); err != nil { 64 return nil, errors.Trace(err) 65 } 66 67 if localState.Kind != operation.Continue { 68 return nil, resolver.ErrNoOperation 69 } 70 71 if err := r.stateTracker.SynchronizeScopes(remoteState); err != nil { 72 return nil, errors.Trace(err) 73 } 74 75 // Check whether we need to fire a hook for any of the relations 76 for relationId, relationSnapshot := range remoteState.Relations { 77 if !r.stateTracker.IsKnown(relationId) { 78 r.logger.Tracef("unknown relation %d resolving next op", relationId) 79 continue 80 } else if isImplicit, _ := r.stateTracker.IsImplicit(relationId); isImplicit { 81 continue 82 } 83 84 // If either the unit or the relation are Dying, or the 85 // relation becomes suspended, then the relation should be 86 // broken. 87 var remoteBroken bool 88 if remoteState.Life == life.Dying || relationSnapshot.Life == life.Dying || relationSnapshot.Suspended { 89 relationSnapshot = remotestate.RelationSnapshot{} 90 remoteBroken = true 91 // TODO(axw) if relation is implicit, leave scope & remove. 92 } 93 94 // Examine local/remote states and figure out if a hook needs 95 // to be fired for this relation. 96 relState, err := r.stateTracker.State(relationId) 97 if err != nil { 98 // 99 relState = NewState(relationId) 100 } 101 hInfo, err := r.nextHookForRelation(relState, relationSnapshot, remoteBroken) 102 if err == resolver.ErrNoOperation { 103 continue 104 } 105 return opFactory.NewRunHook(hInfo) 106 } 107 108 return nil, resolver.ErrNoOperation 109 } 110 111 // maybeDestroySubordinates checks whether the remote state indicates that the 112 // unit is dying and ensures that any related subordinates are properly 113 // destroyed. 114 func (r *relationsResolver) maybeDestroySubordinates(remoteState remotestate.Snapshot) error { 115 if remoteState.Life != life.Dying { 116 return nil 117 } 118 119 var destroyAllSubordinates bool 120 for relationId, relationSnapshot := range remoteState.Relations { 121 if relationSnapshot.Life != life.Alive { 122 continue 123 } else if hasContainerScope, err := r.stateTracker.HasContainerScope(relationId); err != nil || !hasContainerScope { 124 continue 125 } 126 127 // Found alive relation to a subordinate 128 relationSnapshot.Life = life.Dying 129 remoteState.Relations[relationId] = relationSnapshot 130 destroyAllSubordinates = true 131 } 132 133 if destroyAllSubordinates { 134 return r.subordinateDestroyer.DestroyAllSubordinates() 135 } 136 137 return nil 138 } 139 140 func (r *relationsResolver) nextHookForRelation(localState *State, remote remotestate.RelationSnapshot, remoteBroken bool) (hook.Info, error) { 141 // If there's a guaranteed next hook, return that. 142 relationId := localState.RelationId 143 if localState.ChangedPending != "" { 144 // ChangedPending should only happen for a unit (not an app). It is a side effect that if we call 'relation-joined' 145 // for a unit, we immediately queue up relation-changed for that unit, before we run any other hooks 146 // Applications never see "relation-joined". 147 unitName := localState.ChangedPending 148 appName, err := names.UnitApplication(unitName) 149 if err != nil { 150 return hook.Info{}, errors.Annotate(err, "changed pending held an invalid unit name") 151 } 152 return hook.Info{ 153 Kind: hooks.RelationChanged, 154 RelationId: relationId, 155 RemoteUnit: unitName, 156 RemoteApplication: appName, 157 ChangeVersion: remote.Members[unitName], 158 }, nil 159 } 160 161 // Get related app names, trigger all app hooks first 162 allAppNames := set.NewStrings() 163 for appName := range localState.ApplicationMembers { 164 allAppNames.Add(appName) 165 } 166 for app := range remote.ApplicationMembers { 167 allAppNames.Add(app) 168 } 169 sortedAppNames := allAppNames.SortedValues() 170 171 // Get the union of all relevant units, and sort them, so we produce events 172 // in a consistent order (largely for the convenience of the tests). 173 allUnitNames := set.NewStrings() 174 for unitName := range localState.Members { 175 allUnitNames.Add(unitName) 176 } 177 for unitName := range remote.Members { 178 allUnitNames.Add(unitName) 179 } 180 sortedUnitNames := allUnitNames.SortedValues() 181 if allUnitNames.Contains("") { 182 return hook.Info{}, errors.Errorf("somehow we got the empty unit. localState: %v, remote: %v", localState.Members, remote.Members) 183 } 184 185 // If there are any locally known units that are no longer reflected in 186 // remote state, depart them. 187 for _, unitName := range sortedUnitNames { 188 changeVersion, found := localState.Members[unitName] 189 if !found { 190 continue 191 } 192 if _, found := remote.Members[unitName]; !found { 193 appName, err := names.UnitApplication(unitName) 194 if err != nil { 195 return hook.Info{}, errors.Trace(err) 196 } 197 198 // Consult the life of the localState unit and/or app to 199 // figure out if its the localState or the remote unit going 200 // away. Note that if the app is removed, the unit will 201 // still be alive but its parent app will by dying. 202 localUnitLife, localAppLife, err := r.stateTracker.LocalUnitAndApplicationLife() 203 if err != nil { 204 return hook.Info{}, errors.Trace(err) 205 } 206 207 var departee = unitName 208 if localUnitLife != life.Alive || localAppLife != life.Alive { 209 departee = r.stateTracker.LocalUnitName() 210 } 211 212 return hook.Info{ 213 Kind: hooks.RelationDeparted, 214 RelationId: relationId, 215 RemoteUnit: unitName, 216 RemoteApplication: appName, 217 ChangeVersion: changeVersion, 218 DepartingUnit: departee, 219 }, nil 220 } 221 } 222 223 // If the relation's meant to be broken, break it. A side-effect of 224 // the logic that generates the relation-created hooks is that we may 225 // end up in this block for a peer relation. Since you cannot depart 226 // peer relations we can safely ignore this hook. 227 isPeer, _ := r.stateTracker.IsPeerRelation(relationId) 228 if remoteBroken && !isPeer { 229 if !r.stateTracker.StateFound(relationId) { 230 // The relation may have been suspended and then 231 // removed, so we don't want to run the hook twice. 232 return hook.Info{}, resolver.ErrNoOperation 233 } 234 235 return hook.Info{ 236 Kind: hooks.RelationBroken, 237 RelationId: relationId, 238 RemoteApplication: r.stateTracker.RemoteApplication(relationId), 239 }, nil 240 } 241 242 for _, appName := range sortedAppNames { 243 changeVersion, found := remote.ApplicationMembers[appName] 244 if !found { 245 // ? 246 continue 247 } 248 // Note(jam): 2019-10-23 For compatibility purposes, we don't trigger a hook if 249 // localState.ApplicationMembers doesn't contain the app and the changeVersion == 0. 250 // This is because otherwise all charms always get a hook with the app 251 // as the context, and that is likely to expose them to something they 252 // may not be ready for. Also, since no app content has been set, there 253 // is nothing for them to respond to. 254 if oldVersion := localState.ApplicationMembers[appName]; oldVersion != changeVersion { 255 return hook.Info{ 256 Kind: hooks.RelationChanged, 257 RelationId: relationId, 258 RemoteUnit: "", 259 RemoteApplication: appName, 260 ChangeVersion: changeVersion, 261 }, nil 262 } 263 } 264 265 // If there are any remote units not locally known, join them. 266 for _, unitName := range sortedUnitNames { 267 changeVersion, found := remote.Members[unitName] 268 if !found { 269 r.logger.Tracef("cannot join relation %d, no known Members for %q", relationId, unitName) 270 continue 271 } 272 if _, found := localState.Members[unitName]; !found { 273 appName, err := names.UnitApplication(unitName) 274 if err != nil { 275 return hook.Info{}, errors.Trace(err) 276 } 277 return hook.Info{ 278 Kind: hooks.RelationJoined, 279 RelationId: relationId, 280 RemoteUnit: unitName, 281 RemoteApplication: appName, 282 ChangeVersion: changeVersion, 283 }, nil 284 } else { 285 r.logger.Debugf("unit %q already joined relation %d", unitName, relationId) 286 } 287 } 288 289 // Finally scan for remote units whose latest version is not reflected 290 // in localState state. 291 for _, unitName := range sortedUnitNames { 292 remoteChangeVersion, found := remote.Members[unitName] 293 if !found { 294 continue 295 } 296 localChangeVersion, found := localState.Members[unitName] 297 if !found { 298 continue 299 } 300 appName, err := names.UnitApplication(unitName) 301 if err != nil { 302 return hook.Info{}, errors.Trace(err) 303 } 304 // NOTE(axw) we use != and not > to cater due to the 305 // use of the relation settings document's txn-revno 306 // as the version. When model-uuid migration occurs, the 307 // document is recreated, resetting txn-revno. 308 if remoteChangeVersion != localChangeVersion { 309 return hook.Info{ 310 Kind: hooks.RelationChanged, 311 RelationId: relationId, 312 RemoteUnit: unitName, 313 RemoteApplication: appName, 314 ChangeVersion: remoteChangeVersion, 315 }, nil 316 } 317 } 318 319 // Nothing left to do for this relation. 320 return hook.Info{}, resolver.ErrNoOperation 321 } 322 323 // NewCreatedRelationResolver returns a resolver that handles relation-created 324 // hooks and is wired to the provided RelationStateTracker instance. 325 func NewCreatedRelationResolver(stateTracker RelationStateTracker, logger Logger) resolver.Resolver { 326 return &createdRelationsResolver{ 327 stateTracker: stateTracker, 328 logger: logger, 329 } 330 } 331 332 type createdRelationsResolver struct { 333 stateTracker RelationStateTracker 334 logger Logger 335 } 336 337 // NextOp implements resolver.Resolver. 338 func (r *createdRelationsResolver) NextOp( 339 localState resolver.LocalState, 340 remoteState remotestate.Snapshot, 341 opFactory operation.Factory, 342 ) (_ operation.Operation, err error) { 343 if r.logger.IsTraceEnabled() { 344 r.logger.Tracef("create relation resolver next op for new remote relations %# v", pretty.Formatter(remoteState.Relations)) 345 defer func() { 346 if err == resolver.ErrNoOperation { 347 r.logger.Tracef("no create relation operation to run") 348 } 349 }() 350 } 351 // Nothing to do if not yet installed or if the unit is dying. 352 if !localState.Installed || remoteState.Life == life.Dying { 353 return nil, resolver.ErrNoOperation 354 } 355 356 // We should only evaluate the resolver logic if there is no other pending operation 357 if localState.Kind != operation.Continue { 358 return nil, resolver.ErrNoOperation 359 } 360 361 if err := r.stateTracker.SynchronizeScopes(remoteState); err != nil { 362 return nil, errors.Trace(err) 363 } 364 365 for relationId, relationSnapshot := range remoteState.Relations { 366 if relationSnapshot.Life != life.Alive { 367 continue 368 } 369 370 hook, err := r.nextHookForRelation(relationId) 371 if err != nil { 372 if err == resolver.ErrNoOperation { 373 continue 374 } 375 376 return nil, errors.Trace(err) 377 } 378 379 return opFactory.NewRunHook(hook) 380 } 381 382 return nil, resolver.ErrNoOperation 383 } 384 385 func (r *createdRelationsResolver) nextHookForRelation(relationId int) (hook.Info, error) { 386 isImplicit, _ := r.stateTracker.IsImplicit(relationId) 387 if r.stateTracker.RelationCreated(relationId) || isImplicit { 388 return hook.Info{}, resolver.ErrNoOperation 389 } 390 391 return hook.Info{ 392 Kind: hooks.RelationCreated, 393 RelationId: relationId, 394 RemoteApplication: r.stateTracker.RemoteApplication(relationId), 395 }, nil 396 }