1
// Copyright 2012, 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
7
"github.com/juju/errors"
8
jujutxn "github.com/juju/txn"
13
// minUnitsDoc keeps track of relevant changes on the service's MinUnits field
14
// and on the number of alive units for the service.
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 service is never less than the actual
22
// alive units: new units are added if required.
23
type minUnitsDoc struct {
24
// ServiceName 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"`
28
EnvUUID string `bson:"env-uuid"`
32
// SetMinUnits changes the number of minimum units required by the service.
33
func (s *Service) SetMinUnits(minUnits int) (err error) {
34
defer errors.DeferredAnnotatef(&err, "cannot set minimum units for service %q", s)
37
s.doc.MinUnits = minUnits
41
return errors.New("cannot set a negative minimum number of units")
43
service := &Service{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) {
52
if err := service.Refresh(); err != nil {
56
if service.doc.Life != Alive {
57
return nil, errors.New("service is no longer alive")
59
if minUnits == service.doc.MinUnits {
60
return nil, jujutxn.ErrNoOperations
62
return setMinUnitsOps(service, minUnits), nil
64
return s.st.run(buildTxn)
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 {
71
serviceName := service.Name()
74
Id: state.docID(serviceName),
76
Update: bson.D{{"$set", bson.D{{"minunits", minUnits}}}},
78
if service.doc.MinUnits == 0 {
79
return append(ops, txn.Op{
81
Id: state.docID(serviceName),
82
Assert: txn.DocMissing,
84
ServiceName: serviceName,
85
EnvUUID: service.st.EnvironUUID(),
90
return append(ops, minUnitsRemoveOp(state, serviceName))
92
if minUnits > service.doc.MinUnits {
93
op := minUnitsTriggerOp(state, serviceName)
94
op.Assert = txn.DocExists
95
return append(ops, op)
100
// minUnitsTriggerOp returns the operation required to increase the minimum
101
// units revno for the service 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, serviceName string) txn.Op {
109
Id: st.docID(serviceName),
110
Update: bson.D{{"$inc", bson.D{{"revno", 1}}}},
114
// minUnitsRemoveOp returns the operation required to remove the minimum
115
// units document from MongoDB.
116
func minUnitsRemoveOp(st *State, serviceName string) txn.Op {
119
Id: st.docID(serviceName),
124
// MinUnits returns the minimum units count for the service.
125
func (s *Service) MinUnits() int {
126
return s.doc.MinUnits
129
// EnsureMinUnits adds new units if the service's MinUnits value is greater
130
// than the number of alive units.
131
func (s *Service) EnsureMinUnits() (err error) {
132
defer errors.DeferredAnnotatef(&err, "cannot ensure minimum units for service %q", s)
133
service := &Service{st: s.st, doc: s.doc}
135
// Ensure the service is alive.
136
if service.doc.Life != Alive {
137
return errors.New("service is not alive")
139
// Exit without errors if the MinUnits for the service is not set.
140
if service.doc.MinUnits == 0 {
143
// Retrieve the number of alive units for the service.
144
aliveUnits, err := aliveUnitsCount(service)
148
// Calculate the number of required units to be added.
149
missing := service.doc.MinUnits - aliveUnits
153
name, ops, err := ensureMinUnitsOps(service)
158
switch err := s.st.runTransaction(ops); err {
160
// Assign the new unit.
161
unit, err := s.st.Unit(name)
165
if err := service.st.AssignUnit(unit, AssignNew); err != nil {
168
// No need to proceed and refresh the service if this was the
169
// last/only missing unit.
174
// Refresh the service and restart the loop.
178
if err := service.Refresh(); err != nil {
184
// aliveUnitsCount returns the number a alive units for the service.
185
func aliveUnitsCount(service *Service) (int, error) {
186
units, closer := service.st.getCollection(unitsC)
189
query := bson.D{{"service", service.doc.Name}, {"life", Alive}}
190
return units.Find(query).Count()
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 *Service) (string, []txn.Op, error) {
197
asserts := bson.D{{"txn-revno", service.doc.TxnRevno}}
198
return service.addUnitOps("", asserts)