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 }