github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/operation.go (about)

     1  // Copyright 2020 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"strconv"
     8  	"time"
     9  
    10  	"github.com/juju/collections/set"
    11  	"github.com/juju/errors"
    12  	"github.com/juju/mgo/v3"
    13  	"github.com/juju/mgo/v3/bson"
    14  	"github.com/juju/mgo/v3/txn"
    15  	"github.com/juju/names/v5"
    16  	jujutxn "github.com/juju/txn/v3"
    17  )
    18  
    19  // Operation represents a number of tasks resulting from running an action.
    20  // The Operation provides both the justification for individual tasks
    21  // to be performed and the grouping of them.
    22  //
    23  // As an example, if an action is run targeted to several units,
    24  // the operation would reflect the request to run the actions,
    25  // while the individual tasks would track the running of
    26  // the individual actions on each unit.
    27  type Operation interface {
    28  	Entity
    29  
    30  	// Id returns the local id of the Operation.
    31  	Id() string
    32  
    33  	// Enqueued returns the time the operation was added to state.
    34  	Enqueued() time.Time
    35  
    36  	// Started returns the time that the first Action execution began.
    37  	Started() time.Time
    38  
    39  	// Completed returns the completion time of the last Action.
    40  	Completed() time.Time
    41  
    42  	// Summary is the reason for running the operation.
    43  	Summary() string
    44  
    45  	// Fail is why the operation failed.
    46  	Fail() string
    47  
    48  	// Status returns the final state of the operation.
    49  	Status() ActionStatus
    50  
    51  	// OperationTag returns the operation's tag.
    52  	OperationTag() names.OperationTag
    53  
    54  	// Refresh refreshes the contents of the operation.
    55  	Refresh() error
    56  
    57  	// SpawnedTaskCount returns the number of spawned actions.
    58  	SpawnedTaskCount() int
    59  }
    60  
    61  type operationDoc struct {
    62  	DocId     string `bson:"_id"`
    63  	ModelUUID string `bson:"model-uuid"`
    64  
    65  	// Summary is the reason for running the operation.
    66  	Summary string `bson:"summary"`
    67  
    68  	// Fail is why the operation failed.
    69  	Fail string `bson:"fail"`
    70  
    71  	// Enqueued is the time the operation was added.
    72  	Enqueued time.Time `bson:"enqueued"`
    73  
    74  	// Started reflects the time the first action began running.
    75  	Started time.Time `bson:"started"`
    76  
    77  	// Completed reflects the time that the last action was finished.
    78  	Completed time.Time `bson:"completed"`
    79  
    80  	// CompleteTaskCount is used internally for mgo asserts.
    81  	// It is not exposed via the Operation interface.
    82  	CompleteTaskCount int `bson:"complete-task-count"`
    83  
    84  	// Status represents the end state of the Operation.
    85  	// If not explicitly set, this is derived from the
    86  	// status of the associated actions.
    87  	Status ActionStatus `bson:"status"`
    88  
    89  	// SpawnedTaskCount is the number of tasks to be completed in
    90  	// this operation. It is used internally for mgo asserts and
    91  	// not exposed via the Operation interface.
    92  	SpawnedTaskCount int `bson:"spawned-task-count"`
    93  }
    94  
    95  // operation represents a group of associated actions.
    96  type operation struct {
    97  	st         *State
    98  	doc        operationDoc
    99  	taskStatus []ActionStatus
   100  }
   101  
   102  // Id returns the local id of the Operation.
   103  func (op *operation) Id() string {
   104  	return op.st.localID(op.doc.DocId)
   105  }
   106  
   107  // Tag implements the Entity interface and returns a names.Tag that
   108  // is a names.ActionTag.
   109  func (op *operation) Tag() names.Tag {
   110  	return op.OperationTag()
   111  }
   112  
   113  // OperationTag returns the operation's tag.
   114  func (op *operation) OperationTag() names.OperationTag {
   115  	return names.NewOperationTag(op.Id())
   116  }
   117  
   118  // Enqueued returns the time the action was added to state as a pending
   119  // Action.
   120  func (op *operation) Enqueued() time.Time {
   121  	return op.doc.Enqueued
   122  }
   123  
   124  // Started returns the time that the Action execution began.
   125  func (op *operation) Started() time.Time {
   126  	return op.doc.Started
   127  }
   128  
   129  // Completed returns the completion time of the Operation.
   130  func (op *operation) Completed() time.Time {
   131  	return op.doc.Completed
   132  }
   133  
   134  // Summary is the reason for running the operation.
   135  func (op *operation) Summary() string {
   136  	return op.doc.Summary
   137  }
   138  
   139  // Fail is why the operation failed.
   140  func (op *operation) Fail() string {
   141  	return op.doc.Fail
   142  }
   143  
   144  // SpawnedTaskCount is the number of spawned actions.
   145  func (op *operation) SpawnedTaskCount() int {
   146  	return op.doc.SpawnedTaskCount
   147  }
   148  
   149  // Status returns the final state of the operation.
   150  // If not explicitly set, this is derived from the
   151  // status of the associated actions/tasks.
   152  func (op *operation) Status() ActionStatus {
   153  	if op.doc.Status != ActionPending {
   154  		return op.doc.Status
   155  	}
   156  	statusStats := set.NewStrings()
   157  	for _, s := range op.taskStatus {
   158  		statusStats.Add(string(s))
   159  	}
   160  	for _, s := range statusOrder {
   161  		if statusStats.Contains(string(s)) {
   162  			return s
   163  		}
   164  	}
   165  	return op.doc.Status
   166  }
   167  
   168  // Refresh refreshes the contents of the operation.
   169  func (op *operation) Refresh() error {
   170  	doc, taskStatus, err := op.st.getOperationDoc(op.Id())
   171  	if err != nil {
   172  		if errors.IsNotFound(err) {
   173  			return err
   174  		}
   175  		return errors.Annotatef(err, "cannot refresh operation %v", op.Id())
   176  	}
   177  	op.doc = *doc
   178  	op.taskStatus = taskStatus
   179  	return nil
   180  }
   181  
   182  var statusOrder = []ActionStatus{
   183  	ActionError,
   184  	ActionRunning,
   185  	ActionPending,
   186  	ActionFailed,
   187  	ActionCancelled,
   188  	ActionCompleted,
   189  }
   190  
   191  var statusCompletedOrder = []ActionStatus{
   192  	ActionError,
   193  	ActionFailed,
   194  	ActionCancelled,
   195  	ActionCompleted,
   196  }
   197  
   198  // newAction builds an Action for the given State and actionDoc.
   199  func newOperation(st *State, doc operationDoc, taskStatus []ActionStatus) Operation {
   200  	return &operation{
   201  		st:         st,
   202  		doc:        doc,
   203  		taskStatus: taskStatus,
   204  	}
   205  }
   206  
   207  // newOperationDoc builds a new operationDoc.
   208  func newOperationDoc(mb modelBackend, summary string, count int) (operationDoc, string, error) {
   209  	id, err := sequenceWithMin(mb, "task", 1)
   210  	if err != nil {
   211  		return operationDoc{}, "", errors.Trace(err)
   212  	}
   213  	operationID := strconv.Itoa(id)
   214  	modelUUID := mb.ModelUUID()
   215  	return operationDoc{
   216  		DocId:            mb.docID(operationID),
   217  		ModelUUID:        modelUUID,
   218  		Enqueued:         mb.nowToTheSecond(),
   219  		Status:           ActionPending,
   220  		Summary:          summary,
   221  		SpawnedTaskCount: count,
   222  	}, operationID, nil
   223  }
   224  
   225  // EnqueueOperation records the start of an operation.
   226  func (m *Model) EnqueueOperation(summary string, count int) (string, error) {
   227  	var operationID string
   228  	buildTxn := func(attempt int) ([]txn.Op, error) {
   229  		var doc operationDoc
   230  		var err error
   231  		doc, operationID, err = newOperationDoc(m.st, summary, count)
   232  		if err != nil {
   233  			return nil, errors.Trace(err)
   234  		}
   235  
   236  		ops := []txn.Op{{
   237  			C:      operationsC,
   238  			Id:     doc.DocId,
   239  			Assert: txn.DocMissing,
   240  			Insert: doc,
   241  		}}
   242  		return ops, nil
   243  	}
   244  	err := m.st.db().Run(buildTxn)
   245  	return operationID, errors.Trace(err)
   246  }
   247  
   248  // FailOperationEnqueuing sets the operation fail message and updates the
   249  // spawned task count. The spawned task count must be accurate to finalize
   250  // the operation.
   251  func (m *Model) FailOperationEnqueuing(operationID, failMessage string, count int) error {
   252  	operation, err := m.Operation(operationID)
   253  	if err != nil {
   254  		return errors.Trace(err)
   255  	}
   256  	buildTxn := func(attempt int) ([]txn.Op, error) {
   257  		if attempt > 1 {
   258  			if err = operation.Refresh(); err != nil {
   259  				return nil, errors.Trace(err)
   260  			}
   261  		}
   262  		if operation.SpawnedTaskCount() == count {
   263  			return nil, jujutxn.ErrNoOperations
   264  		}
   265  
   266  		update := bson.D{
   267  			{"spawned-task-count", count},
   268  			{"fail", failMessage},
   269  		}
   270  		if count == 0 {
   271  			// If no actions were successfully enqueued, set the operation
   272  			// status to error.
   273  			update = append(update, bson.DocElem{"status", "error"})
   274  		}
   275  
   276  		return []txn.Op{{
   277  			C:      operationsC,
   278  			Id:     m.st.docID(operationID),
   279  			Assert: txn.DocExists,
   280  			Update: bson.D{{"$set", update}},
   281  		}}, nil
   282  	}
   283  	err = m.st.db().Run(buildTxn)
   284  	return errors.Trace(err)
   285  }
   286  
   287  // Operation returns an Operation by Id.
   288  func (m *Model) Operation(id string) (Operation, error) {
   289  	doc, taskStatus, err := m.st.getOperationDoc(id)
   290  	if err != nil {
   291  		return nil, errors.Trace(err)
   292  	}
   293  	return newOperation(m.st, *doc, taskStatus), nil
   294  }
   295  
   296  // OperationWithActions returns an OperationInfo by Id.
   297  func (m *Model) OperationWithActions(id string) (*OperationInfo, error) {
   298  	// First gather the matching actions and record the parent operation ids we need.
   299  	actionsCollection, aCloser := m.st.db().GetCollection(actionsC)
   300  	defer aCloser()
   301  
   302  	var actions []actionDoc
   303  	err := actionsCollection.Find(bson.D{{"operation", id}}).
   304  		Sort("_id").All(&actions)
   305  	if err != nil {
   306  		return nil, errors.Trace(err)
   307  	}
   308  
   309  	operationCollection, oCloser := m.st.db().GetCollection(operationsC)
   310  	defer oCloser()
   311  
   312  	var docs []operationDoc
   313  	err = operationCollection.FindId(id).All(&docs)
   314  	if err != nil {
   315  		return nil, errors.Trace(err)
   316  	}
   317  	if len(docs) == 0 {
   318  		return nil, errors.NotFoundf("operation %v", id)
   319  	}
   320  
   321  	var result OperationInfo
   322  	taskStatus := make([]ActionStatus, len(actions))
   323  	result.Actions = make([]Action, len(actions))
   324  	for i, action := range actions {
   325  		result.Actions[i] = newAction(m.st, action)
   326  		taskStatus[i] = action.Status
   327  	}
   328  	operation := newOperation(m.st, docs[0], taskStatus)
   329  	result.Operation = operation
   330  	return &result, nil
   331  }
   332  
   333  func (st *State) getOperationDoc(id string) (*operationDoc, []ActionStatus, error) {
   334  	operations, closer := st.db().GetCollection(operationsC)
   335  	defer closer()
   336  	actions, closer := st.db().GetCollection(actionsC)
   337  	defer closer()
   338  
   339  	doc := operationDoc{}
   340  	err := operations.FindId(id).One(&doc)
   341  	if err == mgo.ErrNotFound {
   342  		return nil, nil, errors.NotFoundf("operation %q", id)
   343  	}
   344  	if err != nil {
   345  		return nil, nil, errors.Annotatef(err, "cannot get operation %q", id)
   346  	}
   347  	var actionDocs []actionDoc
   348  	err = actions.Find(bson.D{{"operation", id}}).All(&actionDocs)
   349  	if err != nil {
   350  		return nil, nil, errors.Annotatef(err, "cannot get tasks for operation %q", id)
   351  	}
   352  	taskStatus := make([]ActionStatus, len(actionDocs))
   353  	for i, a := range actionDocs {
   354  		taskStatus[i] = a.Status
   355  	}
   356  	return &doc, taskStatus, nil
   357  }
   358  
   359  // AllOperations returns all Operations.
   360  func (m *Model) AllOperations() ([]Operation, error) {
   361  	operations, closer := m.st.db().GetCollection(operationsC)
   362  	defer closer()
   363  
   364  	results := []Operation{}
   365  	docs := []operationDoc{}
   366  	err := operations.Find(nil).All(&docs)
   367  	if err != nil {
   368  		return nil, errors.Annotatef(err, "cannot get all operations")
   369  	}
   370  	for _, doc := range docs {
   371  		// This is for gathering operations for migration - task status values are not relevant
   372  		results = append(results, newOperation(m.st, doc, nil))
   373  	}
   374  	return results, nil
   375  }
   376  
   377  // defaultMaxOperationsLimit is the default maximum number of operations to
   378  // return when performing an operations query.
   379  const defaultMaxOperationsLimit = 50
   380  
   381  // OperationInfo encapsulates an operation and summary
   382  // information about some of its actions.
   383  type OperationInfo struct {
   384  	Operation Operation
   385  	Actions   []Action
   386  }
   387  
   388  // ListOperations returns operations that match the specified criteria.
   389  func (m *Model) ListOperations(
   390  	actionNames []string, actionReceivers []names.Tag, operationStatus []ActionStatus,
   391  	offset, limit int,
   392  ) ([]OperationInfo, bool, error) {
   393  	// First gather the matching actions and record the parent operation ids we need.
   394  	actionsCollection, closer := m.st.db().GetCollection(actionsC)
   395  	defer closer()
   396  
   397  	var receiverIDs []string
   398  	for _, tag := range actionReceivers {
   399  		receiverIDs = append(receiverIDs, tag.Id())
   400  	}
   401  	var receiverTerm, namesTerm bson.D
   402  	if len(receiverIDs) > 0 {
   403  		receiverTerm = bson.D{{"receiver", bson.D{{"$in", receiverIDs}}}}
   404  	}
   405  	if len(actionNames) > 0 {
   406  		namesTerm = bson.D{{"name", bson.D{{"$in", actionNames}}}}
   407  	}
   408  	actionsQuery := append(receiverTerm, namesTerm...)
   409  	var actions []actionDoc
   410  	err := actionsCollection.Find(actionsQuery).
   411  		// For now we'll limit what we return to the caller as action results
   412  		// can be large and show-task can be used to get more detail as needed.
   413  		Select(bson.D{
   414  			{"model-uuid", 0},
   415  			{"messages", 0},
   416  			{"results", 0}}).
   417  		Sort("_id").All(&actions)
   418  	if err != nil {
   419  		return nil, false, errors.Trace(err)
   420  	}
   421  	if len(actions) == 0 {
   422  		return nil, false, nil
   423  	}
   424  
   425  	// We have the operation ids which are parent to any actions matching the criteria.
   426  	// Combine these with additional operation filtering criteria to do the final query.
   427  	operationIds := make([]string, len(actions))
   428  	operationActions := make(map[string][]actionDoc)
   429  	for i, action := range actions {
   430  		operationIds[i] = m.st.docID(action.Operation)
   431  		actions := operationActions[action.Operation]
   432  		actions = append(actions, action)
   433  		operationActions[action.Operation] = actions
   434  	}
   435  
   436  	idsTerm := bson.D{{"_id", bson.D{{"$in", operationIds}}}}
   437  	var statusTerm bson.D
   438  	if len(operationStatus) > 0 {
   439  		statusTerm = bson.D{{"status", bson.D{{"$in", operationStatus}}}}
   440  	}
   441  	operationsQuery := append(idsTerm, statusTerm...)
   442  
   443  	operationCollection, closer := m.st.db().GetCollection(operationsC)
   444  	defer closer()
   445  
   446  	var docs []operationDoc
   447  	query := operationCollection.Find(operationsQuery).Sort("$natural")
   448  	if offset != 0 {
   449  		query = query.Skip(offset)
   450  	}
   451  	// Don't let the user shoot themselves in the foot.
   452  	if limit <= 0 {
   453  		limit = defaultMaxOperationsLimit
   454  	}
   455  	query = query.Limit(limit)
   456  	err = query.All(&docs)
   457  	if err != nil {
   458  		return nil, false, errors.Trace(err)
   459  	}
   460  	query = query.Skip(offset + limit).Limit(1)
   461  	nextCount, err := query.Count()
   462  	if err != nil {
   463  		return nil, false, errors.Trace(err)
   464  	}
   465  	truncated := nextCount != 0
   466  
   467  	result := make([]OperationInfo, len(docs))
   468  	for i, doc := range docs {
   469  		actions := operationActions[m.st.localID(doc.DocId)]
   470  		taskStatus := make([]ActionStatus, len(actions))
   471  		result[i].Actions = make([]Action, len(actions))
   472  		for j, action := range actions {
   473  			result[i].Actions[j] = newAction(m.st, action)
   474  			taskStatus[j] = action.Status
   475  		}
   476  		operation := newOperation(m.st, doc, taskStatus)
   477  		result[i].Operation = operation
   478  	}
   479  	return result, truncated, nil
   480  }