github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/action.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "fmt" 8 "strconv" 9 "time" 10 11 "github.com/juju/collections/set" 12 "github.com/juju/errors" 13 "github.com/juju/loggo" 14 "github.com/juju/mgo/v3" 15 "github.com/juju/mgo/v3/bson" 16 "github.com/juju/mgo/v3/txn" 17 "github.com/juju/names/v5" 18 jujutxn "github.com/juju/txn/v3" 19 20 stateerrors "github.com/juju/juju/state/errors" 21 ) 22 23 const ( 24 actionMarker = "_a_" 25 ) 26 27 var actionLogger = loggo.GetLogger("juju.state.action") 28 29 // ActionStatus represents the possible end states for an action. 30 type ActionStatus string 31 32 const ( 33 // ActionError signifies that the action did not get run due to an error. 34 ActionError ActionStatus = "error" 35 36 // ActionFailed signifies that the action did not complete successfully. 37 ActionFailed ActionStatus = "failed" 38 39 // ActionCompleted indicates that the action ran to completion as intended. 40 ActionCompleted ActionStatus = "completed" 41 42 // ActionCancelled means that the Action was cancelled before being run. 43 ActionCancelled ActionStatus = "cancelled" 44 45 // ActionPending is the default status when an Action is first queued. 46 ActionPending ActionStatus = "pending" 47 48 // ActionRunning indicates that the Action is currently running. 49 ActionRunning ActionStatus = "running" 50 51 // ActionAborting indicates that the Action is running but should be 52 // aborted. 53 ActionAborting ActionStatus = "aborting" 54 55 // ActionAborted indicates the Action was aborted. 56 ActionAborted ActionStatus = "aborted" 57 ) 58 59 var activeStatus = set.NewStrings( 60 string(ActionPending), 61 string(ActionRunning), 62 string(ActionAborting), 63 ) 64 65 type actionNotificationDoc struct { 66 // DocId is the composite _id that can be matched by an 67 // idPrefixWatcher that is configured to watch for the 68 // ActionReceiver Name() which makes up the first part of this 69 // composite _id. 70 DocId string `bson:"_id"` 71 72 // ModelUUID is the model identifier. 73 ModelUUID string `bson:"model-uuid"` 74 75 // Receiver is the Name of the Unit or any other ActionReceiver for 76 // which this notification is queued. 77 Receiver string `bson:"receiver"` 78 79 // ActionID is the unique identifier for the Action this notification 80 // represents. 81 ActionID string `bson:"actionid"` 82 83 // Changed represents the time when the corresponding Action had a change 84 // worthy of notifying on. 85 // NOTE: changed should not be set on pending actions, see actionNotificationWatcher. 86 Changed time.Time `bson:"changed,omitempty"` 87 } 88 89 type actionDoc struct { 90 // DocId is the key for this document; it is a UUID. 91 DocId string `bson:"_id"` 92 93 // ModelUUID is the model identifier. 94 ModelUUID string `bson:"model-uuid"` 95 96 // Receiver is the Name of the Unit or any other ActionReceiver for 97 // which this Action is queued. 98 Receiver string `bson:"receiver"` 99 100 // Name identifies the action that should be run; it should 101 // match an action defined by the unit's charm. 102 Name string `bson:"name"` 103 104 // Parameters holds the action's parameters, if any; it should validate 105 // against the schema defined by the named action in the unit's charm. 106 Parameters map[string]interface{} `bson:"parameters"` 107 108 // Parallel is true if this action can run in parallel with others 109 // without requiring the mandatory acquisition of the machine lock. 110 Parallel bool `bson:"parallel,omitempty"` 111 112 // ExecutionGroup is used to group all actions which require the 113 // same machine lock, ie actions in the same group cannot run in 114 // in parallel with each other. 115 ExecutionGroup string `bson:"execution-group,omitempty"` 116 117 // Enqueued is the time the action was added. 118 Enqueued time.Time `bson:"enqueued"` 119 120 // Started reflects the time the action began running. 121 Started time.Time `bson:"started"` 122 123 // Completed reflects the time that the action was finished. 124 Completed time.Time `bson:"completed"` 125 126 // Operation is the parent operation of the action. 127 Operation string `bson:"operation"` 128 129 // Status represents the end state of the Action; ActionFailed for an 130 // action that was removed prematurely, or that failed, and 131 // ActionCompleted for an action that successfully completed. 132 Status ActionStatus `bson:"status"` 133 134 // Message captures any error returned by the action. 135 Message string `bson:"message"` 136 137 // Results are the structured results from the action. 138 Results map[string]interface{} `bson:"results"` 139 140 // Logs holds the progress messages logged by the action. 141 Logs []ActionMessage `bson:"messages"` 142 } 143 144 // ActionMessage represents a progress message logged by an action. 145 type ActionMessage struct { 146 MessageValue string `bson:"message"` 147 TimestampValue time.Time `bson:"timestamp"` 148 } 149 150 // Timestamp returns the message timestamp. 151 func (m ActionMessage) Timestamp() time.Time { 152 return m.TimestampValue 153 } 154 155 // Message returns the message string. 156 func (m ActionMessage) Message() string { 157 return m.MessageValue 158 } 159 160 // action represents an instruction to do some "action" and is expected 161 // to match an action definition in a charm. 162 type action struct { 163 st *State 164 doc actionDoc 165 } 166 167 // Refresh the contents of the action. 168 func (a *action) Refresh() error { 169 actions, closer := a.st.db().GetCollection(actionsC) 170 defer closer() 171 id := a.Id() 172 doc := actionDoc{} 173 err := actions.FindId(id).One(&doc) 174 if err == mgo.ErrNotFound { 175 return errors.NotFoundf("action %q", id) 176 } 177 if err != nil { 178 return errors.Annotatef(err, "cannot get action %q", id) 179 } 180 a.doc = doc 181 return nil 182 } 183 184 // Id returns the local id of the Action. 185 func (a *action) Id() string { 186 return a.st.localID(a.doc.DocId) 187 } 188 189 // Receiver returns the Name of the ActionReceiver for which this action 190 // is enqueued. Usually this is a Unit Name(). 191 func (a *action) Receiver() string { 192 return a.doc.Receiver 193 } 194 195 // Name returns the name of the action, as defined in the charm. 196 func (a *action) Name() string { 197 return a.doc.Name 198 } 199 200 // Parameters will contain a structure representing arguments or parameters to 201 // an action, and is expected to be validated by the Unit using the Charm 202 // definition of the Action. 203 func (a *action) Parameters() map[string]interface{} { 204 return a.doc.Parameters 205 } 206 207 // Parallel returns true if the action can run without 208 // needed to acquire the machine lock. 209 func (a *action) Parallel() bool { 210 return a.doc.Parallel 211 } 212 213 // ExecutionGroup is the group of actions which cannot 214 // execute in parallel with each other. 215 func (a *action) ExecutionGroup() string { 216 return a.doc.ExecutionGroup 217 } 218 219 // Enqueued returns the time the action was added to state as a pending 220 // Action. 221 func (a *action) Enqueued() time.Time { 222 return a.doc.Enqueued 223 } 224 225 // Started returns the time that the Action execution began. 226 func (a *action) Started() time.Time { 227 return a.doc.Started 228 } 229 230 // Completed returns the completion time of the Action. 231 func (a *action) Completed() time.Time { 232 return a.doc.Completed 233 } 234 235 // Status returns the final state of the action. 236 func (a *action) Status() ActionStatus { 237 return a.doc.Status 238 } 239 240 // Results returns the structured output of the action and any error. 241 func (a *action) Results() (map[string]interface{}, string) { 242 return a.doc.Results, a.doc.Message 243 } 244 245 // Tag implements the Entity interface and returns a names.Tag that 246 // is a names.ActionTag. 247 func (a *action) Tag() names.Tag { 248 return a.ActionTag() 249 } 250 251 // ActionTag returns an ActionTag constructed from this action's 252 // Prefix and Sequence. 253 func (a *action) ActionTag() names.ActionTag { 254 return names.NewActionTag(a.Id()) 255 } 256 257 // Model returns the model associated with the action 258 func (a *action) Model() (*Model, error) { 259 return a.st.Model() 260 } 261 262 // ActionResults is a data transfer object that holds the key Action 263 // output and results information. 264 type ActionResults struct { 265 Status ActionStatus `json:"status"` 266 Results map[string]interface{} `json:"results"` 267 Message string `json:"message"` 268 } 269 270 // Begin marks an action as running, and logs the time it was started. 271 // It asserts that the action is currently pending. 272 func (a *action) Begin() (Action, error) { 273 m, err := a.Model() 274 if err != nil { 275 return nil, errors.Trace(err) 276 } 277 parentOperation, err := m.Operation(a.doc.Operation) 278 if err != nil && !errors.IsNotFound(err) { 279 return nil, errors.Trace(err) 280 } 281 startedTime := a.st.nowToTheSecond() 282 buildTxn := func(attempt int) ([]txn.Op, error) { 283 // If this is the first action to be marked as running 284 // for the parent operation, the operation itself is 285 // marked as running also. 286 var updateOperationOp *txn.Op 287 var err error 288 if parentOperation != nil { 289 if attempt > 0 { 290 err = parentOperation.Refresh() 291 if err != nil && !errors.IsNotFound(err) { 292 return nil, errors.Trace(err) 293 } 294 } 295 parentOpDetails, ok := parentOperation.(*operation) 296 if !ok { 297 return nil, errors.Errorf("parentOperation must be of type operation") 298 } 299 if err == nil && parentOpDetails.doc.Status == ActionPending { 300 updateOperationOp = &txn.Op{ 301 C: operationsC, 302 Id: a.st.docID(parentOperation.Id()), 303 Assert: bson.D{{"status", ActionPending}}, 304 Update: bson.D{{"$set", bson.D{ 305 {"status", ActionRunning}, 306 {"started", startedTime}, 307 }}}, 308 } 309 } 310 } 311 ops := []txn.Op{ 312 { 313 C: actionsC, 314 Id: a.doc.DocId, 315 Assert: bson.D{{"status", ActionPending}}, 316 Update: bson.D{{"$set", bson.D{ 317 {"status", ActionRunning}, 318 {"started", startedTime}, 319 }}}, 320 }} 321 if updateOperationOp != nil { 322 ops = append(ops, *updateOperationOp) 323 } 324 return ops, nil 325 } 326 if err = m.st.db().Run(buildTxn); err != nil { 327 return nil, errors.Trace(err) 328 } 329 return m.Action(a.Id()) 330 } 331 332 // Finish removes action from the pending queue and captures the output 333 // and end state of the action. 334 func (a *action) Finish(results ActionResults) (Action, error) { 335 return a.removeAndLog(results.Status, results.Results, results.Message) 336 } 337 338 // Cancel or Abort the action. 339 func (a *action) Cancel() (Action, error) { 340 m, err := a.Model() 341 if err != nil { 342 return nil, errors.Trace(err) 343 } 344 345 parentOperation, err := m.Operation(a.doc.Operation) 346 if err != nil && !errors.IsNotFound(err) { 347 return nil, errors.Trace(err) 348 } 349 350 cancelTime := a.st.nowToTheSecond() 351 removeAndLog := a.removeAndLogBuildTxn(ActionCancelled, nil, "action cancelled via the API", 352 m, parentOperation, cancelTime) 353 buildTxn := func(attempt int) ([]txn.Op, error) { 354 err := a.Refresh() 355 if err != nil { 356 return nil, errors.Trace(err) 357 } 358 switch a.Status() { 359 case ActionRunning: 360 ops := []txn.Op{ 361 { 362 C: actionsC, 363 Id: a.doc.DocId, 364 Assert: bson.D{{"status", ActionRunning}}, 365 Update: bson.D{{"$set", bson.D{ 366 {"status", ActionAborting}, 367 }}}, 368 }, { 369 C: actionNotificationsC, 370 Id: m.st.docID(ensureActionMarker(a.Receiver()) + a.Id()), 371 Update: bson.D{{"$set", bson.D{ 372 {"changed", cancelTime}, 373 }}}, 374 }, 375 } 376 return ops, nil 377 case ActionPending: 378 return removeAndLog(attempt) 379 default: 380 // Already done. 381 return nil, nil 382 } 383 } 384 if err = m.st.db().Run(buildTxn); err != nil { 385 return nil, errors.Trace(err) 386 } 387 return m.Action(a.Id()) 388 } 389 390 // removeAndLog takes the action off of the pending queue, and creates 391 // an actionresult to capture the outcome of the action. It asserts that 392 // the action is not already completed. 393 func (a *action) removeAndLog(finalStatus ActionStatus, results map[string]interface{}, message string) (Action, error) { 394 m, err := a.Model() 395 if err != nil { 396 return nil, errors.Trace(err) 397 } 398 399 parentOperation, err := m.Operation(a.doc.Operation) 400 if err != nil && !errors.IsNotFound(err) { 401 return nil, errors.Trace(err) 402 } 403 404 completedTime := a.st.nowToTheSecond() 405 buildTxn := a.removeAndLogBuildTxn(finalStatus, results, message, m, parentOperation, completedTime) 406 if err = m.st.db().Run(buildTxn); err != nil { 407 return nil, errors.Trace(err) 408 } 409 return m.Action(a.Id()) 410 } 411 412 // removeAndLogBuildTxn is shared by Cancel and removeAndLog to correctly finalise an action and it's parent op. 413 func (a *action) removeAndLogBuildTxn(finalStatus ActionStatus, results map[string]interface{}, message string, 414 m *Model, parentOperation Operation, completedTime time.Time) jujutxn.TransactionSource { 415 return func(attempt int) ([]txn.Op, error) { 416 // If the action is already finished, return an error. 417 if a.Status() == finalStatus { 418 return nil, errors.NewAlreadyExists(nil, fmt.Sprintf("action %v already has status %q", a.Id(), finalStatus)) 419 } 420 if attempt > 0 { 421 err := a.Refresh() 422 if err != nil { 423 return nil, errors.Trace(err) 424 } 425 if !activeStatus.Contains(string(a.Status())) { 426 return nil, errors.NewAlreadyExists(nil, fmt.Sprintf("action %v is already finished with status %q", a.Id(), finalStatus)) 427 } 428 } 429 assertNotComplete := bson.D{{"status", bson.D{ 430 {"$nin", []interface{}{ 431 ActionCompleted, 432 ActionCancelled, 433 ActionFailed, 434 ActionAborted, 435 ActionError, 436 }}}}} 437 // If this is the last action to be marked as completed 438 // for the parent operation, the operation itself is also 439 // marked as complete. 440 var updateOperationOp *txn.Op 441 var err error 442 if parentOperation != nil { 443 parentOpDetails, ok := parentOperation.(*operation) 444 if !ok { 445 return nil, errors.Errorf("parentOperation must be of type operation") 446 } 447 if attempt > 0 { 448 err = parentOperation.Refresh() 449 if err != nil && !errors.IsNotFound(err) { 450 return nil, errors.Trace(err) 451 } 452 } 453 tasks := parentOpDetails.taskStatus 454 statusStats := set.NewStrings(string(finalStatus)) 455 var numComplete int 456 for _, status := range tasks { 457 statusStats.Add(string(status)) 458 if !activeStatus.Contains(string(status)) { 459 numComplete++ 460 } 461 } 462 spawnedTaskCount := parentOpDetails.doc.SpawnedTaskCount 463 if numComplete == spawnedTaskCount-1 { 464 // Set the operation status based on the individual 465 // task status values. eg if any task is failed, 466 // the entire operation is considered failed. 467 finalOperationStatus := finalStatus 468 for _, s := range statusCompletedOrder { 469 if statusStats.Contains(string(s)) { 470 finalOperationStatus = s 471 break 472 } 473 } 474 if finalOperationStatus == ActionCompleted && parentOpDetails.Fail() != "" { 475 // If an action fails enqueuing, there may not be a doc 476 // to reference. It will only be noted in the operation 477 // fail string. 478 finalOperationStatus = ActionError 479 } 480 updateOperationOp = &txn.Op{ 481 C: operationsC, 482 Id: a.st.docID(parentOperation.Id()), 483 Assert: append(assertNotComplete, 484 bson.DocElem{"complete-task-count", bson.D{{"$eq", spawnedTaskCount - 1}}}), 485 Update: bson.D{{"$set", bson.D{ 486 {"status", finalOperationStatus}, 487 {"completed", completedTime}, 488 {"complete-task-count", spawnedTaskCount}, 489 }}}, 490 } 491 } else { 492 updateOperationOp = &txn.Op{ 493 C: operationsC, 494 Id: a.st.docID(parentOperation.Id()), 495 Assert: append(assertNotComplete, 496 bson.DocElem{"complete-task-count", 497 bson.D{{"$lt", spawnedTaskCount - 1}}}), 498 Update: bson.D{{"$inc", bson.D{ 499 {"complete-task-count", 1}, 500 }}}, 501 } 502 } 503 } 504 ops := []txn.Op{ 505 { 506 C: actionsC, 507 Id: a.doc.DocId, 508 Assert: assertNotComplete, 509 Update: bson.D{{"$set", bson.D{ 510 {"status", finalStatus}, 511 {"message", message}, 512 {"results", results}, 513 {"completed", completedTime}, 514 }}}, 515 }, { 516 C: actionNotificationsC, 517 Id: m.st.docID(ensureActionMarker(a.Receiver()) + a.Id()), 518 Remove: true, 519 }} 520 if updateOperationOp != nil { 521 ops = append(ops, *updateOperationOp) 522 } 523 return ops, nil 524 } 525 } 526 527 // Messages returns the action's progress messages. 528 func (a *action) Messages() []ActionMessage { 529 // Timestamps are not decoded as UTC, so we need to convert :-( 530 result := make([]ActionMessage, len(a.doc.Logs)) 531 for i, m := range a.doc.Logs { 532 result[i] = ActionMessage{ 533 MessageValue: m.MessageValue, 534 TimestampValue: m.TimestampValue.UTC(), 535 } 536 } 537 return result 538 } 539 540 // Log adds message to the action's progress message array. 541 func (a *action) Log(message string) error { 542 // Just to ensure we do not allow bad actions to fill up disk. 543 // 1000 messages should be enough for anyone. 544 if len(a.doc.Logs) > 1000 { 545 logger.Warningf("exceeded 1000 log messages, action may be stuck") 546 return nil 547 } 548 m, err := a.st.Model() 549 if err != nil { 550 return errors.Trace(err) 551 } 552 buildTxn := func(attempt int) ([]txn.Op, error) { 553 if attempt > 0 { 554 anAction, err := m.Action(a.Id()) 555 if err != nil { 556 return nil, errors.Trace(err) 557 } 558 a = anAction.(*action) 559 } 560 if s := a.Status(); s != ActionRunning && s != ActionAborting { 561 return nil, errors.Errorf("cannot log message to task %q with status %v", a.Id(), s) 562 } 563 ops := []txn.Op{ 564 { 565 C: actionsC, 566 Id: a.doc.DocId, 567 Assert: bson.D{{"$or", []bson.D{ 568 {{"status", ActionRunning}}, 569 {{"status", ActionAborting}}, 570 }}}, 571 Update: bson.D{{"$push", bson.D{ 572 {"messages", ActionMessage{MessageValue: message, TimestampValue: a.st.nowToTheSecond().UTC()}}, 573 }}}, 574 }} 575 return ops, nil 576 } 577 err = a.st.db().Run(buildTxn) 578 return errors.Trace(err) 579 } 580 581 // newAction builds an Action for the given State and actionDoc. 582 func newAction(st *State, adoc actionDoc) Action { 583 return &action{ 584 st: st, 585 doc: adoc, 586 } 587 } 588 589 // newActionDoc builds the actionDoc with the given name and parameters. 590 func newActionDoc(mb modelBackend, operationID string, receiverTag names.Tag, 591 actionName string, parameters map[string]interface{}, parallel bool, executionGroup string) (actionDoc, actionNotificationDoc, error) { 592 prefix := ensureActionMarker(receiverTag.Id()) 593 id, err := sequenceWithMin(mb, "task", 1) 594 if err != nil { 595 return actionDoc{}, actionNotificationDoc{}, err 596 } 597 actionId := strconv.Itoa(id) 598 actionLogger.Debugf("newActionDoc name: '%s', receiver: '%s', actionId: '%s'", actionName, receiverTag, actionId) 599 modelUUID := mb.ModelUUID() 600 return actionDoc{ 601 DocId: mb.docID(actionId), 602 ModelUUID: modelUUID, 603 Receiver: receiverTag.Id(), 604 Name: actionName, 605 Parameters: parameters, 606 Parallel: parallel, 607 ExecutionGroup: executionGroup, 608 Enqueued: mb.nowToTheSecond(), 609 Operation: operationID, 610 Status: ActionPending, 611 }, actionNotificationDoc{ 612 DocId: mb.docID(prefix + actionId), 613 ModelUUID: modelUUID, 614 Receiver: receiverTag.Id(), 615 ActionID: actionId, 616 }, nil 617 } 618 619 var ensureActionMarker = ensureSuffixFn(actionMarker) 620 621 // Action returns an Action by Id. 622 func (m *Model) Action(id string) (Action, error) { 623 actionLogger.Tracef("Action() %q", id) 624 st := m.st 625 actions, closer := st.db().GetCollection(actionsC) 626 defer closer() 627 628 doc := actionDoc{} 629 err := actions.FindId(id).One(&doc) 630 if err == mgo.ErrNotFound { 631 return nil, errors.NotFoundf("action %q", id) 632 } 633 if err != nil { 634 return nil, errors.Annotatef(err, "cannot get action %q", id) 635 } 636 actionLogger.Tracef("Action() %q found %+v", id, doc) 637 return newAction(st, doc), nil 638 } 639 640 // AllActions returns all Actions. 641 func (m *Model) AllActions() ([]Action, error) { 642 actionLogger.Tracef("AllActions()") 643 actions, closer := m.st.db().GetCollection(actionsC) 644 defer closer() 645 646 results := []Action{} 647 docs := []actionDoc{} 648 err := actions.Find(nil).All(&docs) 649 if err != nil { 650 return nil, errors.Annotatef(err, "cannot get all actions") 651 } 652 for _, doc := range docs { 653 results = append(results, newAction(m.st, doc)) 654 } 655 return results, nil 656 } 657 658 // ActionByTag returns an Action given an ActionTag. 659 func (m *Model) ActionByTag(tag names.ActionTag) (Action, error) { 660 return m.Action(tag.Id()) 661 } 662 663 // FindActionsByName finds Actions with the given name. 664 func (m *Model) FindActionsByName(name string) ([]Action, error) { 665 var results []Action 666 var doc actionDoc 667 668 actions, closer := m.st.db().GetCollection(actionsC) 669 defer closer() 670 671 iter := actions.Find(bson.D{{"name", name}}).Iter() 672 for iter.Next(&doc) { 673 results = append(results, newAction(m.st, doc)) 674 } 675 return results, errors.Trace(iter.Close()) 676 } 677 678 // EnqueueAction caches the action doc to the database. 679 func (m *Model) EnqueueAction(operationID string, receiver names.Tag, 680 actionName string, payload map[string]interface{}, parallel bool, executionGroup string, actionError error) (Action, error) { 681 if len(actionName) == 0 { 682 return nil, errors.New("action name required") 683 } 684 685 receiverCollectionName, receiverId, err := m.st.tagToCollectionAndId(receiver) 686 if err != nil { 687 return nil, errors.Trace(err) 688 } 689 doc, ndoc, err := newActionDoc(m.st, operationID, receiver, actionName, payload, parallel, executionGroup) 690 if err != nil { 691 return nil, errors.Trace(err) 692 } 693 694 if actionError != nil { 695 doc.Status = ActionError 696 doc.Message = actionError.Error() 697 } 698 ops := []txn.Op{{ 699 C: receiverCollectionName, 700 Id: receiverId, 701 Assert: notDeadDoc, 702 }, { 703 C: operationsC, 704 Id: m.st.docID(operationID), 705 Assert: txn.DocExists, 706 }, { 707 C: actionsC, 708 Id: doc.DocId, 709 Assert: txn.DocMissing, 710 Insert: doc, 711 }} 712 if actionError == nil { 713 ops = append(ops, txn.Op{ 714 C: actionNotificationsC, 715 Id: ndoc.DocId, 716 Assert: txn.DocMissing, 717 Insert: ndoc, 718 }) 719 } 720 721 buildTxn := func(attempt int) ([]txn.Op, error) { 722 if notDead, err := isNotDead(m.st, receiverCollectionName, receiverId); err != nil { 723 return nil, err 724 } else if !notDead { 725 return nil, stateerrors.ErrDead 726 } else if attempt != 0 { 727 _, err := m.Operation(operationID) 728 if err != nil { 729 return nil, errors.Trace(err) 730 } 731 return nil, errors.Errorf("unexpected attempt number '%d'", attempt) 732 } 733 return ops, nil 734 } 735 if err = m.st.db().Run(buildTxn); err == nil { 736 return newAction(m.st, doc), nil 737 } 738 return nil, err 739 } 740 741 // AddAction adds a new Action of type name and using arguments payload to 742 // the receiver, and returns its ID. 743 func (m *Model) AddAction(receiver ActionReceiver, operationID, name string, payload map[string]interface{}, parallel *bool, executionGroup *string) (Action, error) { 744 payload, runParallel, runExecutionGroup, err := receiver.PrepareActionPayload(name, payload, parallel, executionGroup) 745 if err != nil { 746 _, err2 := m.EnqueueAction(operationID, receiver.Tag(), name, payload, runParallel, runExecutionGroup, err) 747 if err2 != nil { 748 err = err2 749 } 750 return nil, errors.Trace(err) 751 } 752 return m.EnqueueAction(operationID, receiver.Tag(), name, payload, runParallel, runExecutionGroup, nil) 753 } 754 755 // matchingActions finds actions that match ActionReceiver. 756 func (st *State) matchingActions(ar ActionReceiver) ([]Action, error) { 757 return st.matchingActionsByReceiverId(ar.Tag().Id()) 758 } 759 760 // matchingActionsByReceiverId finds actions that match ActionReceiver name. 761 func (st *State) matchingActionsByReceiverId(id string) ([]Action, error) { 762 var doc actionDoc 763 var actions []Action 764 765 actionsCollection, closer := st.db().GetCollection(actionsC) 766 defer closer() 767 768 iter := actionsCollection.Find(bson.D{{"receiver", id}}).Iter() 769 for iter.Next(&doc) { 770 actions = append(actions, newAction(st, doc)) 771 } 772 return actions, errors.Trace(iter.Close()) 773 } 774 775 // matchingActionsPending finds actions that match ActionReceiver and 776 // that are pending. 777 func (st *State) matchingActionsPending(ar ActionReceiver) ([]Action, error) { 778 pending := bson.D{{"status", ActionPending}} 779 return st.matchingActionsByReceiverAndStatus(ar.Tag(), pending) 780 } 781 782 // matchingActionsRunning finds actions that match ActionReceiver and 783 // that are running. 784 func (st *State) matchingActionsRunning(ar ActionReceiver) ([]Action, error) { 785 running := bson.D{{"$or", []bson.D{ 786 {{"status", ActionRunning}}, 787 {{"status", ActionAborting}}, 788 }}} 789 return st.matchingActionsByReceiverAndStatus(ar.Tag(), running) 790 } 791 792 // matchingActionsCompleted finds actions that match ActionReceiver and 793 // that are complete. 794 func (st *State) matchingActionsCompleted(ar ActionReceiver) ([]Action, error) { 795 completed := bson.D{{"$or", []bson.D{ 796 {{"status", ActionCompleted}}, 797 {{"status", ActionCancelled}}, 798 {{"status", ActionFailed}}, 799 {{"status", ActionAborted}}, 800 }}} 801 return st.matchingActionsByReceiverAndStatus(ar.Tag(), completed) 802 } 803 804 // matchingActionsByReceiverAndStatus finds actionNotifications that 805 // match ActionReceiver. 806 func (st *State) matchingActionsByReceiverAndStatus(tag names.Tag, statusCondition bson.D) ([]Action, error) { 807 var doc actionDoc 808 var actions []Action 809 810 actionsCollection, closer := st.db().GetCollection(actionsC) 811 defer closer() 812 813 sel := append(bson.D{{"receiver", tag.Id()}}, statusCondition...) 814 iter := actionsCollection.Find(sel).Iter() 815 816 for iter.Next(&doc) { 817 actions = append(actions, newAction(st, doc)) 818 } 819 return actions, errors.Trace(iter.Close()) 820 } 821 822 // PruneOperations removes operation entries and their sub-tasks until 823 // only logs newer than <maxLogTime> remain and also ensures 824 // that the actions collection is smaller than <maxLogsMB> after the deletion. 825 func PruneOperations(stop <-chan struct{}, st *State, maxHistoryTime time.Duration, maxHistoryMB int) error { 826 // There may be older actions without parent operations so try those first. 827 hasNoOperation := bson.D{{"$or", []bson.D{ 828 {{"operation", ""}}, 829 {{"operation", bson.D{{"$exists", false}}}}, 830 }}} 831 coll, closer := st.db().GetRawCollection(actionsC) 832 defer closer() 833 err := pruneCollection(stop, st, maxHistoryTime, maxHistoryMB, coll, "completed", hasNoOperation, GoTime) 834 if err != nil { 835 return errors.Trace(err) 836 } 837 // First calculate the average ratio of tasks to operations. Since deletion is 838 // done at the operation level, and any associated tasks are then deleted, but 839 // the actions collection is where the disk space goes, we approximate the 840 // number of operations to delete to achieve a given size deduction based on 841 // the average ratio of number of operations to tasks. 842 operationsColl, closer := st.db().GetRawCollection(operationsC) 843 defer closer() 844 operationsCount, err := operationsColl.Count() 845 if err != nil { 846 return errors.Annotate(err, "retrieving operations collection count") 847 } 848 actionsColl, closer := st.db().GetRawCollection(actionsC) 849 defer closer() 850 actionsCount, err := actionsColl.Count() 851 if err != nil { 852 return errors.Annotate(err, "retrieving actions collection count") 853 } 854 sizeFactor := float64(actionsCount) / float64(operationsCount) 855 856 err = pruneCollectionAndChildren(stop, st, maxHistoryTime, maxHistoryMB, operationsColl, actionsColl, "completed", "operation", nil, sizeFactor, GoTime) 857 return errors.Trace(err) 858 }