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