github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/state/raftlease/target.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package raftlease 5 6 import ( 7 "fmt" 8 "io" 9 "log" 10 11 "github.com/juju/errors" 12 "github.com/juju/loggo" 13 jujutxn "github.com/juju/txn" 14 "gopkg.in/mgo.v2" 15 "gopkg.in/mgo.v2/bson" 16 "gopkg.in/mgo.v2/txn" 17 18 "github.com/juju/juju/core/lease" 19 "github.com/juju/juju/core/raftlease" 20 "github.com/juju/juju/mongo" 21 ) 22 23 const ( 24 // fieldNamespace identifies the namespace field in a leaseHolderDoc. 25 fieldNamespace = "namespace" 26 27 // fieldModelUUID identifies the model UUID field in a leaseHolderDoc. 28 fieldModelUUID = "model-uuid" 29 30 // fieldHolder identifies the holder field in a leaseHolderDoc. 31 fieldHolder = "holder" 32 ) 33 34 // logger is only used when we need to update the database from within 35 // a trapdoor function. 36 var logger = loggo.GetLogger("juju.state.raftlease") 37 38 // leaseHolderDoc is used to serialise lease holder info. 39 type leaseHolderDoc struct { 40 Id string `bson:"_id"` 41 Namespace string `bson:"namespace"` 42 ModelUUID string `bson:"model-uuid"` 43 Lease string `bson:"lease"` 44 Holder string `bson:"holder"` 45 } 46 47 // leaseHolderDocId returns the _id for the document holding details of the supplied 48 // namespace and lease. 49 func leaseHolderDocId(namespace, modelUUID, lease string) string { 50 return fmt.Sprintf("%s:%s#%s#", modelUUID, namespace, lease) 51 } 52 53 // validate returns an error if any fields are invalid or inconsistent. 54 func (doc leaseHolderDoc) validate() error { 55 if doc.Id != leaseHolderDocId(doc.Namespace, doc.ModelUUID, doc.Lease) { 56 return errors.Errorf("inconsistent _id") 57 } 58 if err := lease.ValidateString(doc.Holder); err != nil { 59 return errors.Annotatef(err, "invalid holder") 60 } 61 return nil 62 } 63 64 // newLeaseHolderDoc returns a valid lease document encoding the supplied lease and 65 // entry in the supplied namespace, or an error. 66 func newLeaseHolderDoc(namespace, modelUUID, name, holder string) (*leaseHolderDoc, error) { 67 doc := &leaseHolderDoc{ 68 Id: leaseHolderDocId(namespace, modelUUID, name), 69 Namespace: namespace, 70 ModelUUID: modelUUID, 71 Lease: name, 72 Holder: holder, 73 } 74 if err := doc.validate(); err != nil { 75 return nil, errors.Trace(err) 76 } 77 return doc, nil 78 } 79 80 // Mongo exposes MongoDB operations for use by the lease package. 81 type Mongo interface { 82 83 // RunTransaction should probably delegate to a jujutxn.Runner's Run method. 84 RunTransaction(jujutxn.TransactionSource) error 85 86 // GetCollection should probably call the mongo.CollectionFromName func. 87 GetCollection(name string) (collection mongo.Collection, closer func()) 88 } 89 90 // Logger allows us to report errors if we can't write to the database 91 // for some reason. 92 type Logger interface { 93 Errorf(string, ...interface{}) 94 } 95 96 // NewNotifyTarget returns something that can be used to report lease 97 // changes. 98 func NewNotifyTarget(mongo Mongo, collection string, logDest io.Writer, errorLogger Logger) raftlease.NotifyTarget { 99 return ¬ifyTarget{ 100 mongo: mongo, 101 collection: collection, 102 logger: log.New(logDest, "", log.LstdFlags|log.Lmicroseconds|log.LUTC), 103 errorLogger: errorLogger, 104 } 105 } 106 107 // notifyTarget is a raftlease.NotifyTarget that updates the 108 // information in mongo, as well as logging the lease changes. Since 109 // the callbacks it exposes aren't allowed to return errors, it takes 110 // a logger for write errors as well as a destination for tracing 111 // lease changes. 112 type notifyTarget struct { 113 mongo Mongo 114 collection string 115 logger *log.Logger 116 errorLogger Logger 117 } 118 119 func (t *notifyTarget) log(message string, args ...interface{}) { 120 err := t.logger.Output(2, fmt.Sprintf(message, args...)) 121 if err != nil { 122 t.errorLogger.Errorf("couldn't write to lease log: %s", err.Error()) 123 } 124 } 125 126 func buildClaimedOps(coll mongo.Collection, docId string, key lease.Key, holder string) ([]txn.Op, error) { 127 existingDoc, err := getRecord(coll, docId) 128 switch { 129 case err == mgo.ErrNotFound: 130 doc, err := newLeaseHolderDoc( 131 key.Namespace, 132 key.ModelUUID, 133 key.Lease, 134 holder, 135 ) 136 if err != nil { 137 return nil, errors.Trace(err) 138 } 139 return []txn.Op{{ 140 C: coll.Name(), 141 Id: docId, 142 Assert: txn.DocMissing, 143 Insert: doc, 144 }}, nil 145 146 case err != nil: 147 return nil, errors.Trace(err) 148 149 case existingDoc.Holder == holder: 150 return nil, jujutxn.ErrNoOperations 151 152 default: 153 return []txn.Op{{ 154 C: coll.Name(), 155 Id: docId, 156 Assert: bson.M{ 157 fieldHolder: existingDoc.Holder, 158 }, 159 Update: bson.M{ 160 "$set": bson.M{ 161 fieldHolder: holder, 162 }, 163 }, 164 }}, nil 165 } 166 } 167 168 func applyClaimed(mongo Mongo, collection string, docId string, key lease.Key, holder string) (bool, error) { 169 coll, closer := mongo.GetCollection(collection) 170 defer closer() 171 var writeNeeded bool 172 err := mongo.RunTransaction(func(int) ([]txn.Op, error) { 173 ops, err := buildClaimedOps(coll, docId, key, holder) 174 writeNeeded = len(ops) != 0 175 return ops, err 176 }) 177 return writeNeeded, errors.Trace(err) 178 } 179 180 // Claimed is part of raftlease.NotifyTarget. 181 func (t *notifyTarget) Claimed(key lease.Key, holder string) { 182 docId := leaseHolderDocId(key.Namespace, key.ModelUUID, key.Lease) 183 t.log("claimed %q for %q", docId, holder) 184 _, err := applyClaimed(t.mongo, t.collection, docId, key, holder) 185 if err != nil { 186 t.errorLogger.Errorf("couldn't claim lease %q for %q in db: %s", docId, holder, err.Error()) 187 t.log("couldn't claim lease %q for %q in db: %s", docId, holder, err.Error()) 188 return 189 } 190 } 191 192 // Expired is part of raftlease.NotifyTarget. 193 func (t *notifyTarget) Expired(key lease.Key) { 194 coll, closer := t.mongo.GetCollection(t.collection) 195 defer closer() 196 docId := leaseHolderDocId(key.Namespace, key.ModelUUID, key.Lease) 197 t.log("expired %q", docId) 198 err := t.mongo.RunTransaction(func(_ int) ([]txn.Op, error) { 199 existingDoc, err := getRecord(coll, docId) 200 if err == mgo.ErrNotFound { 201 return nil, jujutxn.ErrNoOperations 202 } 203 if err != nil { 204 return nil, errors.Trace(err) 205 } 206 return []txn.Op{{ 207 C: t.collection, 208 Id: docId, 209 Assert: bson.M{ 210 fieldHolder: existingDoc.Holder, 211 }, 212 Remove: true, 213 }}, nil 214 }) 215 216 if err != nil { 217 t.errorLogger.Errorf("couldn't expire lease %q in db: %s", docId, err.Error()) 218 t.log("couldn't expire lease %q in db: %s", docId, err.Error()) 219 return 220 } 221 } 222 223 // MakeTrapdoorFunc returns a raftlease.TrapdoorFunc for the specified 224 // collection. 225 func MakeTrapdoorFunc(mongo Mongo, collection string) raftlease.TrapdoorFunc { 226 return func(key lease.Key, holder string) lease.Trapdoor { 227 return func(attempt int, out interface{}) error { 228 outPtr, ok := out.(*[]txn.Op) 229 if !ok { 230 return errors.NotValidf("expected *[]txn.Op; %T", out) 231 } 232 if attempt != 0 { 233 // If the assertion failed it may be because a claim 234 // notify failed in the past due to the DB not being 235 // available. Sync the lease holder - this is safe to 236 // do because raft is the arbiter of who really holds 237 // the lease, and we check that the lease is held in 238 // buildTxnWithLeadership each time before collecting 239 // the assertion ops. 240 docId := leaseHolderDocId(key.Namespace, key.ModelUUID, key.Lease) 241 writeNeeded, err := applyClaimed(mongo, collection, docId, key, holder) 242 if err != nil { 243 return errors.Trace(err) 244 } 245 if writeNeeded { 246 logger.Infof("trapdoor claimed lease %q for %q", docId, holder) 247 } 248 } 249 *outPtr = []txn.Op{{ 250 C: collection, 251 Id: leaseHolderDocId( 252 key.Namespace, 253 key.ModelUUID, 254 key.Lease, 255 ), 256 Assert: bson.M{ 257 fieldHolder: holder, 258 }, 259 }} 260 return nil 261 } 262 } 263 } 264 265 func getRecord(coll mongo.Collection, docId string) (leaseHolderDoc, error) { 266 var doc leaseHolderDoc 267 err := coll.FindId(docId).One(&doc) 268 if err != nil { 269 return leaseHolderDoc{}, err 270 } 271 return doc, nil 272 } 273 274 // LeaseHolders returns a map of each lease and the holder in the 275 // specified namespace and model. 276 func LeaseHolders(mongo Mongo, collection, namespace, modelUUID string) (map[string]string, error) { 277 coll, closer := mongo.GetCollection(collection) 278 defer closer() 279 280 iter := coll.Find(bson.M{ 281 fieldNamespace: namespace, 282 fieldModelUUID: modelUUID, 283 }).Iter() 284 results := make(map[string]string) 285 var doc leaseHolderDoc 286 for iter.Next(&doc) { 287 results[doc.Lease] = doc.Holder 288 } 289 290 if err := iter.Close(); err != nil { 291 return nil, errors.Trace(err) 292 } 293 return results, nil 294 }