github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/sequence.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 "fmt" 8 9 "github.com/juju/errors" 10 "github.com/juju/mgo/v3" 11 "github.com/juju/mgo/v3/bson" 12 ) 13 14 type sequenceDoc struct { 15 DocID string `bson:"_id"` 16 Name string `bson:"name"` 17 ModelUUID string `bson:"model-uuid"` 18 Counter int // This stores the *next* value to return. 19 } 20 21 // sequence safely increments a database backed sequence, returning 22 // the next value. 23 func sequence(mb modelBackend, name string) (int, error) { 24 sequences, closer := mb.db().GetCollection(sequenceC) 25 defer closer() 26 query := sequences.FindId(name) 27 inc := mgo.Change{ 28 Update: bson.M{ 29 "$set": bson.M{ 30 "name": name, 31 "model-uuid": mb.ModelUUID(), 32 }, 33 "$inc": bson.M{"counter": 1}, 34 }, 35 Upsert: true, 36 } 37 result := &sequenceDoc{} 38 _, err := query.Apply(inc, result) 39 if err != nil { 40 return -1, fmt.Errorf("cannot increment %q sequence number: %v", name, err) 41 } 42 return result.Counter, nil 43 } 44 45 func resetSequence(mb modelBackend, name string) error { 46 sequences, closer := mb.db().GetCollection(sequenceC) 47 defer closer() 48 err := sequences.Writeable().RemoveId(name) 49 if err != nil && errors.Cause(err) != mgo.ErrNotFound { 50 return errors.Annotatef(err, "can not reset sequence for %q", name) 51 } 52 return nil 53 } 54 55 // sequenceWithMin safely increments a database backed sequence, 56 // allowing for a minimum value for the sequence to be specified. The 57 // minimum value is used as an initial value for the first use of a 58 // particular sequence. The minimum value will also cause a sequence 59 // value to jump ahead if the minimum is provided that is higher than 60 // the current sequence value. 61 // 62 // The data manipulated by `sequence` and `sequenceWithMin` is the 63 // same. It is safe to mix the 2 methods for the same sequence. 64 // 65 // `sequence` is more efficient than `sequenceWithMin` and should be 66 // preferred if there is no minimum value requirement. 67 func sequenceWithMin(mb modelBackend, name string, minVal int) (int, error) { 68 sequences, closer := mb.db().GetRawCollection(sequenceC) 69 defer closer() 70 updater := newDbSeqUpdater(sequences, mb.ModelUUID(), name) 71 return updateSeqWithMin(updater, minVal) 72 } 73 74 // seqUpdater abstracts away the database operations required for 75 // updating a sequence. 76 type seqUpdater interface { 77 // read returns the current value of the sequence. If the sequence 78 // doesn't exist yet it returns 0. 79 read() (int, error) 80 81 // create attempts to create a new sequence with the initial value 82 // provided. It returns (true, nil) on success, (false, nil) if 83 // the sequence already existed and (false, <some error>) if any 84 // other error occurred. 85 create(value int) (bool, error) 86 87 // set attempts to update the sequence value to a new value. It 88 // takes the expected current value of the sequence as well as the 89 // new value to set it to. (true, nil) is returned if the value 90 // was updated successfully. (false, nil) is returned if the 91 // sequence was not at the expected value (indicating a concurrent 92 // update). (false, <some error>) is returned for any other 93 // problem. 94 set(expected, next int) (bool, error) 95 } 96 97 // Sequences returns the model's sequence names and their next values. 98 func (st *State) Sequences() (map[string]int, error) { 99 sequences, closer := st.db().GetCollection(sequenceC) 100 defer closer() 101 102 var docs []sequenceDoc 103 if err := sequences.Find(nil).All(&docs); err != nil { 104 return nil, errors.Trace(err) 105 } 106 107 result := make(map[string]int) 108 for _, doc := range docs { 109 result[doc.Name] = doc.Counter 110 } 111 return result, nil 112 } 113 114 const maxSeqRetries = 20 115 116 // updateSeqWithMin implements the abstract logic for incrementing a 117 // database backed sequence in a concurrency aware way. 118 // 119 // It is complicated because MongoDB's atomic update primitives don't 120 // provide a way to upsert a counter while also providing an initial 121 // value. Instead, a number of database operations are used for each 122 // sequence update, relying on the atomicity guarantees that MongoDB 123 // offers. Optimistic database updates are attempted with retries when 124 // contention is observed. 125 func updateSeqWithMin(sequence seqUpdater, minVal int) (int, error) { 126 for try := 0; try < maxSeqRetries; try++ { 127 curVal, err := sequence.read() 128 if err != nil { 129 return -1, errors.Annotate(err, "could not read sequence") 130 } 131 if curVal == 0 { 132 // No sequence document exists, create one. 133 ok, err := sequence.create(minVal + 1) 134 if err != nil { 135 return -1, errors.Annotate(err, "could not create sequence") 136 } 137 if ok { 138 return minVal, nil 139 } 140 // Someone else created the sequence document at the same 141 // time, try again. 142 } else { 143 // Increment an existing sequence document, respecting the 144 // minimum value provided. 145 nextVal := curVal + 1 146 if nextVal <= minVal { 147 nextVal = minVal + 1 148 } 149 ok, err := sequence.set(curVal, nextVal) 150 if err != nil { 151 return -1, errors.Annotate(err, "could not set sequence") 152 } 153 if ok { 154 return nextVal - 1, nil 155 } 156 // Someone else incremented the sequence at the same time, 157 // try again. 158 } 159 } 160 return -1, errors.New("too much contention while updating sequence") 161 } 162 163 // dbSeqUpdater implements seqUpdater. 164 type dbSeqUpdater struct { 165 coll *mgo.Collection 166 modelUUID string 167 name string 168 id string 169 } 170 171 func newDbSeqUpdater(coll *mgo.Collection, modelUUID, name string) *dbSeqUpdater { 172 return &dbSeqUpdater{ 173 coll: coll, 174 modelUUID: modelUUID, 175 name: name, 176 id: modelUUID + ":" + name, 177 } 178 } 179 180 func (su *dbSeqUpdater) read() (int, error) { 181 var doc bson.M 182 err := su.coll.FindId(su.id).One(&doc) 183 if errors.Cause(err) == mgo.ErrNotFound { 184 return 0, nil 185 } else if err != nil { 186 return -1, errors.Trace(err) 187 } 188 return doc["counter"].(int), nil 189 } 190 191 func (su *dbSeqUpdater) create(value int) (bool, error) { 192 err := su.coll.Insert(bson.M{ 193 "_id": su.id, 194 "name": su.name, 195 "model-uuid": su.modelUUID, 196 "counter": value, 197 }) 198 if mgo.IsDup(errors.Cause(err)) { 199 return false, nil 200 } else if err != nil { 201 return false, errors.Trace(err) 202 } 203 return true, nil 204 } 205 206 func (su *dbSeqUpdater) set(expected, next int) (bool, error) { 207 err := su.coll.Update( 208 bson.M{"_id": su.id, "counter": expected}, 209 bson.M{"$set": bson.M{"counter": next}}, 210 ) 211 if errors.Cause(err) == mgo.ErrNotFound { 212 return false, nil 213 } else if err != nil { 214 return false, errors.Trace(err) 215 } 216 return true, nil 217 } 218 219 func (su *dbSeqUpdater) ensure(next int) error { 220 curVal, err := su.read() 221 if err != nil { 222 return errors.Trace(err) 223 } 224 225 var ok bool 226 if curVal == 0 { 227 ok, err = su.create(next) 228 } else { 229 // Sequences should never go backwards. 230 if next <= curVal { 231 return nil 232 } 233 ok, err = su.set(curVal, next) 234 } 235 if !ok { 236 return errors.New("unexpected contention while updating sequence") 237 } 238 return errors.Trace(err) 239 }