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 }