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  }