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