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  }