github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/minimumunits.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package state 5 6 import ( 7 "github.com/juju/errors" 8 "github.com/juju/mgo/v3" 9 "github.com/juju/mgo/v3/bson" 10 "github.com/juju/mgo/v3/txn" 11 jujutxn "github.com/juju/txn/v3" 12 ) 13 14 // minUnitsDoc keeps track of relevant changes on the application's MinUnits field 15 // and on the number of alive units for the application. 16 // A new document is created when MinUnits is set to a non zero value. 17 // A document is deleted when either the associated application is destroyed 18 // or MinUnits is restored to zero. The Revno is increased when either MinUnits 19 // for a application is increased or a unit is destroyed. 20 // TODO(frankban): the MinUnitsWatcher reacts to changes by sending events, 21 // each one describing one or more application. A worker reacts to those events 22 // ensuring the number of units for the application is never less than the actual 23 // alive units: new units are added if required. 24 type minUnitsDoc struct { 25 // ApplicationName is safe to be used here in place of its globalKey, since 26 // the referred entity type is always the Application. 27 DocID string `bson:"_id"` 28 ApplicationName string 29 ModelUUID string `bson:"model-uuid"` 30 Revno int 31 } 32 33 // SetMinUnits changes the number of minimum units required by the application. 34 func (a *Application) SetMinUnits(minUnits int) (err error) { 35 defer errors.DeferredAnnotatef(&err, "cannot set minimum units for application %q", a) 36 defer func() { 37 if err == nil { 38 a.doc.MinUnits = minUnits 39 } 40 }() 41 if minUnits < 0 { 42 return errors.New("cannot set a negative minimum number of units") 43 } 44 app := &Application{st: a.st, doc: a.doc} 45 // Removing the document never fails. Racing clients trying to create the 46 // document generate one failure, but the second attempt should succeed. 47 // If one client tries to update the document, and a racing client removes 48 // it, the former should be able to re-create the document in the second 49 // attempt. If the referred-to application advanced its life cycle to a not 50 // alive state, an error is returned after the first failing attempt. 51 buildTxn := func(attempt int) ([]txn.Op, error) { 52 if attempt > 0 { 53 if err := app.Refresh(); err != nil { 54 return nil, err 55 } 56 } 57 if app.doc.Life != Alive { 58 return nil, errors.New("application is no longer alive") 59 } 60 if minUnits == app.doc.MinUnits { 61 return nil, jujutxn.ErrNoOperations 62 } 63 return setMinUnitsOps(app, minUnits), nil 64 } 65 return a.st.db().Run(buildTxn) 66 } 67 68 // setMinUnitsOps returns the operations required to set MinUnits on the 69 // application and to create/update/remove the minUnits document in MongoDB. 70 func setMinUnitsOps(app *Application, minUnits int) []txn.Op { 71 state := app.st 72 applicationname := app.Name() 73 ops := []txn.Op{{ 74 C: applicationsC, 75 Id: state.docID(applicationname), 76 Assert: isAliveDoc, 77 Update: bson.D{{"$set", bson.D{{"minunits", minUnits}}}}, 78 }} 79 if app.doc.MinUnits == 0 { 80 return append(ops, txn.Op{ 81 C: minUnitsC, 82 Id: state.docID(applicationname), 83 Assert: txn.DocMissing, 84 Insert: &minUnitsDoc{ 85 ApplicationName: applicationname, 86 ModelUUID: app.st.ModelUUID(), 87 }, 88 }) 89 } 90 if app.doc.MinUnits > 0 && minUnits == 0 { 91 return append(ops, minUnitsRemoveOp(state, applicationname)) 92 } 93 if minUnits > app.doc.MinUnits { 94 op := minUnitsTriggerOp(state, applicationname) 95 op.Assert = txn.DocExists 96 return append(ops, op) 97 } 98 return ops 99 } 100 101 // doesMinUnitsExits checks if the minUnits doc exists in the database. 102 func doesMinUnitsExist(st *State, appName string) (bool, error) { 103 minUnits, closer := st.db().GetCollection(minUnitsC) 104 defer closer() 105 var result bson.D 106 err := minUnits.FindId(appName).Select(bson.M{"_id": 1}).One(&result) 107 if err == nil { 108 return true, nil 109 } else if err == mgo.ErrNotFound { 110 return false, nil 111 } else { 112 return false, errors.Trace(err) 113 } 114 } 115 116 // minUnitsTriggerOp returns the operation required to increase the minimum 117 // units revno for the application in MongoDB. Note that this doesn't mean the 118 // minimum number of units is changing, just the evaluation revno is being 119 // incremented, so things maintaining stasis will wake up and respond. 120 // This is included in the operations performed when a unit is 121 // destroyed: if the document exists, then we need to update the Revno. 122 func minUnitsTriggerOp(st *State, applicationname string) txn.Op { 123 return txn.Op{ 124 C: minUnitsC, 125 Id: st.docID(applicationname), 126 Assert: txn.DocExists, 127 Update: bson.D{{"$inc", bson.D{{"revno", 1}}}}, 128 } 129 } 130 131 // minUnitsRemoveOp returns the operation required to remove the minimum 132 // units document from MongoDB. 133 func minUnitsRemoveOp(st *State, applicationname string) txn.Op { 134 return txn.Op{ 135 C: minUnitsC, 136 Id: st.docID(applicationname), 137 Assert: txn.DocExists, 138 Remove: true, 139 } 140 } 141 142 // MinUnits returns the minimum units count for the application. 143 func (a *Application) MinUnits() int { 144 return a.doc.MinUnits 145 } 146 147 // EnsureMinUnits adds new units if the application's MinUnits value is greater 148 // than the number of alive units. 149 func (a *Application) EnsureMinUnits() (err error) { 150 defer errors.DeferredAnnotatef(&err, "cannot ensure minimum units for application %q", a) 151 app := &Application{st: a.st, doc: a.doc} 152 for { 153 // Ensure the application is alive. 154 if app.doc.Life != Alive { 155 return errors.New("application is not alive") 156 } 157 // Exit without errors if the MinUnits for the application is not set. 158 if app.doc.MinUnits == 0 { 159 return nil 160 } 161 // Retrieve the number of alive units for the application. 162 aliveUnits, err := aliveUnitsCount(app) 163 if err != nil { 164 return err 165 } 166 // Calculate the number of required units to be added. 167 missing := app.doc.MinUnits - aliveUnits 168 if missing <= 0 { 169 return nil 170 } 171 name, ops, err := ensureMinUnitsOps(app) 172 if err != nil { 173 return err 174 } 175 // Add missing unit. 176 switch err := a.st.db().RunTransaction(ops); err { 177 case nil: 178 // Assign the new unit. 179 unit, err := a.st.Unit(name) 180 if err != nil { 181 return err 182 } 183 if err := app.st.AssignUnit(unit, AssignNew); err != nil { 184 return err 185 } 186 // No need to proceed and refresh the application if this was the 187 // last/only missing unit. 188 if missing == 1 { 189 return nil 190 } 191 case txn.ErrAborted: 192 // Refresh the application and restart the loop. 193 default: 194 return err 195 } 196 if err := app.Refresh(); err != nil { 197 return err 198 } 199 } 200 } 201 202 // aliveUnitsCount returns the number a alive units for the application. 203 func aliveUnitsCount(app *Application) (int, error) { 204 units, closer := app.st.db().GetCollection(unitsC) 205 defer closer() 206 207 query := bson.D{{"application", app.doc.Name}, {"life", Alive}} 208 return units.Find(query).Count() 209 } 210 211 // ensureMinUnitsOps returns the operations required to add a unit for the 212 // application in MongoDB and the name for the new unit. The resulting transaction 213 // will be aborted if the application document changes when running the operations. 214 func ensureMinUnitsOps(app *Application) (string, []txn.Op, error) { 215 asserts := bson.D{{"txn-revno", app.doc.TxnRevno}} 216 return app.addUnitOps("", AddUnitParams{}, asserts) 217 }