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")