github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/state/lease/client.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package lease 5 6 import ( 7 "fmt" 8 "time" 9 10 "github.com/juju/errors" 11 "github.com/juju/loggo" 12 jujutxn "github.com/juju/txn" 13 "gopkg.in/mgo.v2" 14 "gopkg.in/mgo.v2/bson" 15 "gopkg.in/mgo.v2/txn" 16 17 "github.com/juju/juju/core/lease" 18 "github.com/juju/juju/mongo" 19 ) 20 21 // NewClient returns a new Client using the supplied config, or an error. Any 22 // of the following situations will prevent client creation: 23 // * invalid config 24 // * invalid clock data stored in the namespace 25 // * invalid lease data stored in the namespace 26 // ...but a returned Client will hold a recent cache of lease data and be ready 27 // to use. 28 // Clients do not need to be cleaned up themselves, but they will not function 29 // past the lifetime of their configured Mongo. 30 func NewClient(config ClientConfig) (lease.Client, error) { 31 if err := config.validate(); err != nil { 32 return nil, errors.Trace(err) 33 } 34 loggerName := fmt.Sprintf("state.lease.%s.%s", config.Namespace, config.Id) 35 logger := loggo.GetLogger(loggerName) 36 client := &client{ 37 config: config, 38 logger: logger, 39 } 40 if err := client.ensureClockDoc(); err != nil { 41 return nil, errors.Trace(err) 42 } 43 if err := client.Refresh(); err != nil { 44 return nil, errors.Trace(err) 45 } 46 return client, nil 47 } 48 49 // client implements the lease.Client interface. 50 type client struct { 51 52 // config holds resources and configuration necessary to store leases. 53 config ClientConfig 54 55 // logger holds a logger unique to this lease Client. 56 logger loggo.Logger 57 58 // entries records recent information about leases. 59 entries map[string]entry 60 61 // skews records recent information about remote writers' clocks. 62 skews map[string]Skew 63 } 64 65 // Leases is part of the lease.Client interface. 66 func (client *client) Leases() map[string]lease.Info { 67 leases := make(map[string]lease.Info) 68 for name, entry := range client.entries { 69 skew := client.skews[entry.writer] 70 leases[name] = lease.Info{ 71 Holder: entry.holder, 72 Expiry: skew.Latest(entry.expiry), 73 Trapdoor: client.assertOpTrapdoor(name, entry.holder), 74 } 75 } 76 return leases 77 } 78 79 // ClaimLease is part of the lease.Client interface. 80 func (client *client) ClaimLease(name string, request lease.Request) error { 81 return client.request(name, request, client.claimLeaseOps, "claiming") 82 } 83 84 // ExtendLease is part of the lease.Client interface. 85 func (client *client) ExtendLease(name string, request lease.Request) error { 86 return client.request(name, request, client.extendLeaseOps, "extending") 87 } 88 89 // opsFunc is used to make the signature of the request method somewhat readable. 90 type opsFunc func(name string, request lease.Request) ([]txn.Op, entry, error) 91 92 // request implements ClaimLease and ExtendLease. 93 func (client *client) request(name string, request lease.Request, getOps opsFunc, verb string) error { 94 if err := lease.ValidateString(name); err != nil { 95 return errors.Annotatef(err, "invalid name") 96 } 97 if err := request.Validate(); err != nil { 98 return errors.Annotatef(err, "invalid request") 99 } 100 101 // Close over cacheEntry to record in case of success. 102 var cacheEntry entry 103 err := client.config.Mongo.RunTransaction(func(attempt int) ([]txn.Op, error) { 104 client.logger.Tracef("%s lease %q for %s (attempt %d)", verb, name, request, attempt) 105 106 // On the first attempt, assume cache is good. 107 if attempt > 0 { 108 if err := client.Refresh(); err != nil { 109 return nil, errors.Trace(err) 110 } 111 } 112 113 // It's possible that the request is for an "extension" isn't an 114 // extension at all; this isn't a problem, but does require separate 115 // handling. 116 ops, nextEntry, err := getOps(name, request) 117 cacheEntry = nextEntry 118 if errors.Cause(err) == errNoExtension { 119 return nil, jujutxn.ErrNoOperations 120 } 121 if err != nil { 122 return nil, errors.Trace(err) 123 } 124 return ops, nil 125 }) 126 127 if err != nil { 128 if errors.Cause(err) == lease.ErrInvalid { 129 return lease.ErrInvalid 130 } 131 return errors.Annotate(err, "cannot satisfy request") 132 } 133 134 // Update the cache for this lease only. 135 client.entries[name] = cacheEntry 136 return nil 137 } 138 139 // ExpireLease is part of the Client interface. 140 func (client *client) ExpireLease(name string) error { 141 if err := lease.ValidateString(name); err != nil { 142 return errors.Annotatef(err, "invalid name") 143 } 144 145 // No cache updates needed, only deletes; no closure here. 146 err := client.config.Mongo.RunTransaction(func(attempt int) ([]txn.Op, error) { 147 client.logger.Tracef("expiring lease %q (attempt %d)", name, attempt) 148 149 // On the first attempt, assume cache is good. 150 if attempt > 0 { 151 if err := client.Refresh(); err != nil { 152 return nil, errors.Trace(err) 153 } 154 } 155 156 // No special error handling here. 157 ops, err := client.expireLeaseOps(name) 158 if err != nil { 159 return nil, errors.Trace(err) 160 } 161 return ops, nil 162 }) 163 164 if err != nil { 165 if errors.Cause(err) == lease.ErrInvalid { 166 return lease.ErrInvalid 167 } 168 return errors.Trace(err) 169 } 170 171 // Uncache this lease entry. 172 delete(client.entries, name) 173 return nil 174 } 175 176 // Refresh is part of the Client interface. 177 func (client *client) Refresh() error { 178 client.logger.Tracef("refreshing") 179 180 // Always read entries before skews, because skews are written before 181 // entries; we increase the risk of reading older skew data, but (should) 182 // eliminate the risk of reading an entry whose writer is not present 183 // in the skews data. 184 collection, closer := client.config.Mongo.GetCollection(client.config.Collection) 185 defer closer() 186 entries, err := client.readEntries(collection) 187 if err != nil { 188 return errors.Trace(err) 189 } 190 skews, err := client.readSkews(collection) 191 if err != nil { 192 return errors.Trace(err) 193 } 194 195 // Check we're not missing any required clock information before 196 // updating our local state. 197 for name, entry := range entries { 198 if _, found := skews[entry.writer]; !found { 199 return errors.Errorf("lease %q invalid: no clock data for %s", name, entry.writer) 200 } 201 } 202 client.skews = skews 203 client.entries = entries 204 return nil 205 } 206 207 // ensureClockDoc returns an error if it can neither find nor create a 208 // valid clock document for the client's namespace. 209 func (client *client) ensureClockDoc() error { 210 collection, closer := client.config.Mongo.GetCollection(client.config.Collection) 211 defer closer() 212 213 clockDocId := client.clockDocId() 214 err := client.config.Mongo.RunTransaction(func(attempt int) ([]txn.Op, error) { 215 client.logger.Tracef("checking clock %q (attempt %d)", clockDocId, attempt) 216 var clockDoc clockDoc 217 err := collection.FindId(clockDocId).One(&clockDoc) 218 switch err { 219 case nil: 220 client.logger.Tracef("clock already exists") 221 if err := clockDoc.validate(); err != nil { 222 return nil, errors.Annotatef(err, "corrupt clock document") 223 } 224 return nil, jujutxn.ErrNoOperations 225 case mgo.ErrNotFound: 226 client.logger.Tracef("creating clock") 227 newClockDoc, err := newClockDoc(client.config.Namespace) 228 if err != nil { 229 return nil, errors.Trace(err) 230 } 231 return []txn.Op{{ 232 C: client.config.Collection, 233 Id: clockDocId, 234 Assert: txn.DocMissing, 235 Insert: newClockDoc, 236 }}, nil 237 default: 238 return nil, errors.Trace(err) 239 } 240 }) 241 return errors.Trace(err) 242 } 243 244 // readEntries reads all lease data for the client's namespace. 245 func (client *client) readEntries(collection mongo.Collection) (map[string]entry, error) { 246 247 // Read all lease documents in the client's namespace. 248 query := bson.M{ 249 fieldType: typeLease, 250 fieldNamespace: client.config.Namespace, 251 } 252 iter := collection.Find(query).Iter() 253 254 // Extract valid entries for each one. 255 entries := make(map[string]entry) 256 var leaseDoc leaseDoc 257 for iter.Next(&leaseDoc) { 258 name, entry, err := leaseDoc.entry() 259 if err != nil { 260 return nil, errors.Annotatef(err, "corrupt lease document %q", leaseDoc.Id) 261 } 262 entries[name] = entry 263 } 264 if err := iter.Close(); err != nil { 265 return nil, errors.Trace(err) 266 } 267 return entries, nil 268 } 269 270 // readSkews reads all clock data for the client's namespace. 271 func (client *client) readSkews(collection mongo.Collection) (map[string]Skew, error) { 272 273 // Read the clock document, recording the time before and after completion. 274 beginning := client.config.Clock.Now() 275 var clockDoc clockDoc 276 if err := collection.FindId(client.clockDocId()).One(&clockDoc); err != nil { 277 return nil, errors.Trace(err) 278 } 279 end := client.config.Clock.Now() 280 if err := clockDoc.validate(); err != nil { 281 return nil, errors.Annotatef(err, "corrupt clock document") 282 } 283 284 // Create skew entries for each known writer... 285 skews, err := clockDoc.skews(beginning, end) 286 if err != nil { 287 return nil, errors.Trace(err) 288 } 289 290 // If a writer was previously known to us, and has not written since last 291 // time we read, we should keep the original skew, which is more accurate. 292 for writer, skew := range client.skews { 293 if skews[writer].LastWrite == skew.LastWrite { 294 skews[writer] = skew 295 } 296 } 297 298 // ...and overwrite our own with a zero skew, which will DTRT (assuming 299 // nobody's reusing client ids across machines with different clocks, 300 // which *should* never happen). 301 skews[client.config.Id] = Skew{} 302 return skews, nil 303 } 304 305 // claimLeaseOps returns the []txn.Op necessary to claim the supplied lease 306 // until duration in the future, and a cache entry corresponding to the values 307 // that will be written if the transaction succeeds. If the claim would conflict 308 // with cached state, it returns lease.ErrInvalid. 309 func (client *client) claimLeaseOps(name string, request lease.Request) ([]txn.Op, entry, error) { 310 311 // We can't claim a lease that's already held. 312 if _, found := client.entries[name]; found { 313 return nil, entry{}, lease.ErrInvalid 314 } 315 316 // According to the local clock, we want the lease to extend until 317 // <duration> in the future. 318 now := client.config.Clock.Now() 319 expiry := now.Add(request.Duration) 320 nextEntry := entry{ 321 holder: request.Holder, 322 expiry: expiry, 323 writer: client.config.Id, 324 } 325 326 // We need to write the entry to the database in a specific format. 327 leaseDoc, err := newLeaseDoc(client.config.Namespace, name, nextEntry) 328 if err != nil { 329 return nil, entry{}, errors.Trace(err) 330 } 331 extendLeaseOp := txn.Op{ 332 C: client.config.Collection, 333 Id: leaseDoc.Id, 334 Assert: txn.DocMissing, 335 Insert: leaseDoc, 336 } 337 338 // We always write a clock-update operation *before* writing lease info. 339 writeClockOp := client.writeClockOp(now) 340 ops := []txn.Op{writeClockOp, extendLeaseOp} 341 return ops, nextEntry, nil 342 } 343 344 // extendLeaseOps returns the []txn.Op necessary to extend the supplied lease 345 // until duration in the future, and a cache entry corresponding to the values 346 // that will be written if the transaction succeeds. If the supplied lease 347 // already extends far enough that no operations are required, it will return 348 // errNoExtension. If the extension would conflict with cached state, it will 349 // return lease.ErrInvalid. 350 func (client *client) extendLeaseOps(name string, request lease.Request) ([]txn.Op, entry, error) { 351 352 // Reject extensions when there's no lease, or the holder doesn't match. 353 lastEntry, found := client.entries[name] 354 if !found { 355 return nil, entry{}, lease.ErrInvalid 356 } 357 if lastEntry.holder != request.Holder { 358 return nil, entry{}, lease.ErrInvalid 359 } 360 361 // According to the local clock, we want the lease to extend until 362 // <duration> in the future. 363 now := client.config.Clock.Now() 364 expiry := now.Add(request.Duration) 365 366 // We don't know what time the original writer thinks it is, but we 367 // can figure out the earliest and latest local times at which it 368 // could be expecting its original lease to expire. 369 skew := client.skews[lastEntry.writer] 370 if expiry.Before(skew.Earliest(lastEntry.expiry)) { 371 // The "extended" lease will certainly expire before the 372 // existing lease could. Done. 373 return nil, lastEntry, errNoExtension 374 } 375 latestExpiry := skew.Latest(lastEntry.expiry) 376 if expiry.Before(latestExpiry) { 377 // The lease might be long enough, but we're not sure, so we'll 378 // write a new one that definitely is long enough; but we must 379 // be sure that the new lease has an expiry time such that no 380 // other writer can consider it to have expired before the 381 // original writer considers its own lease to have expired. 382 expiry = latestExpiry 383 } 384 385 // We know we need to write a lease; we know when it needs to expire; we 386 // know what needs to go into the local cache: 387 nextEntry := entry{ 388 holder: lastEntry.holder, 389 expiry: expiry, 390 writer: client.config.Id, 391 } 392 393 // ...and what needs to change in the database, and how to ensure the 394 // change is still valid when it's executed. 395 extendLeaseOp := txn.Op{ 396 C: client.config.Collection, 397 Id: client.leaseDocId(name), 398 Assert: bson.M{ 399 fieldLeaseHolder: lastEntry.holder, 400 fieldLeaseExpiry: toInt64(lastEntry.expiry), 401 fieldLeaseWriter: lastEntry.writer, 402 }, 403 Update: bson.M{"$set": bson.M{ 404 fieldLeaseExpiry: toInt64(expiry), 405 fieldLeaseWriter: client.config.Id, 406 }}, 407 } 408 409 // We always write a clock-update operation *before* writing lease info. 410 writeClockOp := client.writeClockOp(now) 411 ops := []txn.Op{writeClockOp, extendLeaseOp} 412 return ops, nextEntry, nil 413 } 414 415 // expireLeaseOps returns the []txn.Op necessary to vacate the lease. If the 416 // expiration would conflict with cached state, it will return an error with 417 // a Cause of ErrInvalid. 418 func (client *client) expireLeaseOps(name string) ([]txn.Op, error) { 419 420 // We can't expire a lease that doesn't exist. 421 lastEntry, found := client.entries[name] 422 if !found { 423 return nil, lease.ErrInvalid 424 } 425 426 // We also can't expire a lease whose expiry time may be in the future. 427 skew := client.skews[lastEntry.writer] 428 latestExpiry := skew.Latest(lastEntry.expiry) 429 now := client.config.Clock.Now() 430 if !now.After(latestExpiry) { 431 return nil, errors.Annotatef(lease.ErrInvalid, "lease %q expires in the future", name) 432 } 433 434 // The database change is simple, and depends on the lease doc being 435 // untouched since we looked: 436 expireLeaseOp := txn.Op{ 437 C: client.config.Collection, 438 Id: client.leaseDocId(name), 439 Assert: bson.M{ 440 fieldLeaseHolder: lastEntry.holder, 441 fieldLeaseExpiry: toInt64(lastEntry.expiry), 442 fieldLeaseWriter: lastEntry.writer, 443 }, 444 Remove: true, 445 } 446 447 // We always write a clock-update operation *before* writing lease info. 448 // Removing a lease document counts as writing lease info. 449 writeClockOp := client.writeClockOp(now) 450 ops := []txn.Op{writeClockOp, expireLeaseOp} 451 return ops, nil 452 } 453 454 // writeClockOp returns a txn.Op which writes the supplied time to the writer's 455 // field in the skew doc, and aborts if a more recent time has been recorded for 456 // that writer. 457 func (client *client) writeClockOp(now time.Time) txn.Op { 458 dbNow := toInt64(now) 459 dbKey := fmt.Sprintf("%s.%s", fieldClockWriters, client.config.Id) 460 return txn.Op{ 461 C: client.config.Collection, 462 Id: client.clockDocId(), 463 Assert: bson.M{ 464 "$or": []bson.M{{ 465 dbKey: bson.M{"$lte": dbNow}, 466 }, { 467 dbKey: bson.M{"$exists": false}, 468 }}, 469 }, 470 Update: bson.M{ 471 "$set": bson.M{dbKey: dbNow}, 472 }, 473 } 474 } 475 476 // assertOpTrapdoor returns a lease.Trapdoor that will replace a supplied 477 // *[]txn.Op with one that asserts that the holder still holds the named lease. 478 func (client *client) assertOpTrapdoor(name, holder string) lease.Trapdoor { 479 op := txn.Op{ 480 C: client.config.Collection, 481 Id: client.leaseDocId(name), 482 Assert: bson.M{ 483 fieldLeaseHolder: holder, 484 }, 485 } 486 return func(out interface{}) error { 487 outPtr, ok := out.(*[]txn.Op) 488 if !ok { 489 return errors.NotValidf("expected *[]txn.Op; %T", out) 490 } 491 *outPtr = []txn.Op{op} 492 return nil 493 } 494 } 495 496 // clockDocId returns the id of the clock document in the client's namespace. 497 func (client *client) clockDocId() string { 498 return clockDocId(client.config.Namespace) 499 } 500 501 // leaseDocId returns the id of the named lease document in the client's 502 // namespace. 503 func (client *client) leaseDocId(name string) string { 504 return leaseDocId(client.config.Namespace, name) 505 } 506 507 // entry holds the details of a lease and how it was written. 508 type entry struct { 509 // holder identifies the current holder of the lease. 510 holder string 511 512 // expiry is the (writer-local) time at which the lease is safe to remove. 513 expiry time.Time 514 515 // writer identifies the client that wrote the lease. 516 writer string 517 } 518 519 // errNoExtension is used internally to avoid running unnecessary transactions. 520 var errNoExtension = errors.New("lease needs no extension")