github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/core/raftlease/fsm.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  	"io"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/hashicorp/raft"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/utils/set"
    14  	yaml "gopkg.in/yaml.v2"
    15  
    16  	"github.com/juju/juju/core/globalclock"
    17  	"github.com/juju/juju/core/lease"
    18  )
    19  
    20  const (
    21  	// CommandVersion is the current version of the command format. If
    22  	// this changes then we need to be sure that reading and applying
    23  	// commands for previous versions still works.
    24  	CommandVersion = 1
    25  
    26  	// SnapshotVersion is the current version of the snapshot
    27  	// format. Similarly, changes to the snapshot representation need
    28  	// to be backward-compatible.
    29  	SnapshotVersion = 1
    30  
    31  	// OperationClaim denotes claiming a new lease.
    32  	OperationClaim = "claim"
    33  
    34  	// OperationExtend denotes extending an already-held lease.
    35  	OperationExtend = "extend"
    36  
    37  	// OperationSetTime denotes updating stored global time (which
    38  	// will also remove any expired leases).
    39  	OperationSetTime = "setTime"
    40  
    41  	// OperationPin pins a lease, preventing it from expiring
    42  	// until it is unpinned.
    43  	OperationPin = "pin"
    44  
    45  	// OperationUnpin unpins a lease, restoring normal
    46  	// lease expiry behaviour.
    47  	OperationUnpin = "unpin"
    48  )
    49  
    50  // FSMResponse defines what will be available on the return value from
    51  // FSM apply calls.
    52  type FSMResponse interface {
    53  	// Error is a lease error (rather than anything to do with the
    54  	// raft machinery).
    55  	Error() error
    56  
    57  	// Notify tells the target what changes occurred because of the
    58  	// applied command.
    59  	Notify(NotifyTarget)
    60  }
    61  
    62  // NewFSM returns a new FSM to store lease information.
    63  func NewFSM() *FSM {
    64  	return &FSM{
    65  		entries: make(map[lease.Key]*entry),
    66  		pinned:  make(map[lease.Key]set.Strings),
    67  	}
    68  }
    69  
    70  // FSM stores the state of leases in the system.
    71  type FSM struct {
    72  	mu         sync.Mutex
    73  	globalTime time.Time
    74  	entries    map[lease.Key]*entry
    75  
    76  	// Pinned leases are denoted by having a non-empty collection of tags
    77  	// representing the applications requiring pinned behaviour,
    78  	// against their key.
    79  	// This allows different Juju concerns to pin leases, but remove only
    80  	// their own pins. It is done to avoid restoring normal expiration
    81  	// to a lease pinned by another concern operating under under the
    82  	// assumption that the lease holder will not change.
    83  	pinned map[lease.Key]set.Strings
    84  }
    85  
    86  func (f *FSM) claim(key lease.Key, holder string, duration time.Duration) *response {
    87  	if _, found := f.entries[key]; found {
    88  		return invalidResponse()
    89  	}
    90  	f.entries[key] = &entry{
    91  		holder:   holder,
    92  		start:    f.globalTime,
    93  		duration: duration,
    94  	}
    95  	return &response{claimed: key, claimer: holder}
    96  }
    97  
    98  func (f *FSM) extend(key lease.Key, holder string, duration time.Duration) *response {
    99  	entry, found := f.entries[key]
   100  	if !found {
   101  		return invalidResponse()
   102  	}
   103  	if entry.holder != holder {
   104  		return invalidResponse()
   105  	}
   106  	expiry := f.globalTime.Add(duration)
   107  	if !expiry.After(entry.start.Add(entry.duration)) {
   108  		// No extension needed - the lease already expires after the
   109  		// new time.
   110  		return &response{}
   111  	}
   112  	// entry is a pointer back into the f.entries map, so this update
   113  	// isn't lost.
   114  	entry.start = f.globalTime
   115  	entry.duration = duration
   116  	return &response{}
   117  }
   118  
   119  func (f *FSM) pin(key lease.Key, entity string) *response {
   120  	if f.pinned[key] == nil {
   121  		f.pinned[key] = set.NewStrings()
   122  	}
   123  	f.pinned[key].Add(entity)
   124  	return &response{}
   125  }
   126  
   127  func (f *FSM) unpin(key lease.Key, entity string) *response {
   128  	if f.pinned[key] != nil {
   129  		f.pinned[key].Remove(entity)
   130  	}
   131  	return &response{}
   132  }
   133  
   134  func (f *FSM) setTime(oldTime, newTime time.Time) *response {
   135  	if f.globalTime != oldTime {
   136  		return &response{err: globalclock.ErrConcurrentUpdate}
   137  	}
   138  	f.globalTime = newTime
   139  	return &response{expired: f.removeExpired(newTime)}
   140  }
   141  
   142  // expired returns a collection of keys for leases that have expired.
   143  // Any pinned leases are not included in the return.
   144  func (f *FSM) removeExpired(newTime time.Time) []lease.Key {
   145  	var expired []lease.Key
   146  	for key, entry := range f.entries {
   147  		expiry := entry.start.Add(entry.duration)
   148  		if expiry.Before(newTime) && !f.isPinned(key) {
   149  			delete(f.entries, key)
   150  			expired = append(expired, key)
   151  		}
   152  	}
   153  	return expired
   154  }
   155  
   156  // GlobalTime returns the FSM's internal time.
   157  func (f *FSM) GlobalTime() time.Time {
   158  	return f.globalTime
   159  }
   160  
   161  // Leases gets information about all of the leases in the system,
   162  // optionally filtered by the input lease keys.
   163  func (f *FSM) Leases(getLocalTime func() time.Time, keys ...lease.Key) map[lease.Key]lease.Info {
   164  	if len(keys) > 0 {
   165  		return f.filteredLeases(getLocalTime, keys)
   166  	}
   167  	return f.allLeases(getLocalTime)
   168  }
   169  
   170  // filteredLeases is an optimisation for anticipated usage.
   171  // There will usually be a single key for filtering, so iterating over the
   172  // filter list and retrieving from entries will be fastest by far.
   173  func (f *FSM) filteredLeases(getLocalTime func() time.Time, keys []lease.Key) map[lease.Key]lease.Info {
   174  	results := make(map[lease.Key]lease.Info)
   175  	f.mu.Lock()
   176  	localTime := getLocalTime()
   177  	for _, key := range keys {
   178  		if entry, ok := f.entries[key]; ok {
   179  			results[key] = f.infoFromEntry(localTime, key, entry)
   180  		}
   181  	}
   182  	f.mu.Unlock()
   183  	return results
   184  }
   185  
   186  func (f *FSM) allLeases(getLocalTime func() time.Time) map[lease.Key]lease.Info {
   187  	results := make(map[lease.Key]lease.Info)
   188  	f.mu.Lock()
   189  	localTime := getLocalTime()
   190  	for key, entry := range f.entries {
   191  		results[key] = f.infoFromEntry(localTime, key, entry)
   192  	}
   193  	f.mu.Unlock()
   194  	return results
   195  }
   196  
   197  func (f *FSM) infoFromEntry(localTime time.Time, key lease.Key, entry *entry) lease.Info {
   198  	globalExpiry := entry.start.Add(entry.duration)
   199  
   200  	// Pinned leases are always represented as having an expiry in the future.
   201  	// This prevents the lease manager from waking up thinking it has some
   202  	// expiry events to handle.
   203  	remaining := globalExpiry.Sub(f.globalTime)
   204  	if f.isPinned(key) {
   205  		remaining = 30 * time.Second
   206  	}
   207  	localExpiry := localTime.Add(remaining)
   208  
   209  	return lease.Info{
   210  		Holder: entry.holder,
   211  		Expiry: localExpiry,
   212  	}
   213  }
   214  
   215  // Pinned returns all of the currently known lease pins and applications
   216  // requiring the pinned behaviour.
   217  func (f *FSM) Pinned() map[lease.Key][]string {
   218  	f.mu.Lock()
   219  	pinned := make(map[lease.Key][]string)
   220  	for key, entities := range f.pinned {
   221  		if !entities.IsEmpty() {
   222  			pinned[key] = entities.SortedValues()
   223  		}
   224  	}
   225  	f.mu.Unlock()
   226  	return pinned
   227  }
   228  
   229  func (f *FSM) isPinned(key lease.Key) bool {
   230  	return !f.pinned[key].IsEmpty()
   231  }
   232  
   233  // entry holds the details of a lease.
   234  type entry struct {
   235  	// holder identifies the current holder of the lease.
   236  	holder string
   237  
   238  	// start is the global time at which the lease started.
   239  	start time.Time
   240  
   241  	// duration is the duration for which the lease is valid,
   242  	// from the start time.
   243  	duration time.Duration
   244  }
   245  
   246  var _ FSMResponse = (*response)(nil)
   247  
   248  // response stores what happened as a result of applying a command.
   249  type response struct {
   250  	err     error
   251  	claimer string
   252  	claimed lease.Key
   253  	expired []lease.Key
   254  }
   255  
   256  // Error is part of FSMResponse.
   257  func (r *response) Error() error {
   258  	return r.err
   259  }
   260  
   261  // Notify is part of FSMResponse.
   262  func (r *response) Notify(target NotifyTarget) {
   263  	// This response is either for a claim (in which case claimer will
   264  	// be set) or a set-time (so it will have zero or more expiries).
   265  	if r.claimer != "" {
   266  		target.Claimed(r.claimed, r.claimer)
   267  	}
   268  	for _, expiredKey := range r.expired {
   269  		target.Expired(expiredKey)
   270  	}
   271  }
   272  
   273  func invalidResponse() *response {
   274  	return &response{err: lease.ErrInvalid}
   275  }
   276  
   277  // Apply is part of raft.FSM.
   278  func (f *FSM) Apply(log *raft.Log) interface{} {
   279  	var command Command
   280  	err := yaml.Unmarshal(log.Data, &command)
   281  	if err != nil {
   282  		return &response{err: errors.Trace(err)}
   283  	}
   284  	if err := command.Validate(); err != nil {
   285  		return &response{err: errors.Trace(err)}
   286  	}
   287  
   288  	f.mu.Lock()
   289  	defer f.mu.Unlock()
   290  
   291  	switch command.Operation {
   292  	case OperationClaim:
   293  		return f.claim(command.LeaseKey(), command.Holder, command.Duration)
   294  	case OperationExtend:
   295  		return f.extend(command.LeaseKey(), command.Holder, command.Duration)
   296  	case OperationPin:
   297  		return f.pin(command.LeaseKey(), command.PinEntity)
   298  	case OperationUnpin:
   299  		return f.unpin(command.LeaseKey(), command.PinEntity)
   300  	case OperationSetTime:
   301  		return f.setTime(command.OldTime, command.NewTime)
   302  	default:
   303  		return &response{err: errors.NotValidf("operation %q", command.Operation)}
   304  	}
   305  }
   306  
   307  // Snapshot is part of raft.FSM.
   308  func (f *FSM) Snapshot() (raft.FSMSnapshot, error) {
   309  	f.mu.Lock()
   310  
   311  	entries := make(map[SnapshotKey]SnapshotEntry, len(f.entries))
   312  	for key, entry := range f.entries {
   313  		entries[SnapshotKey{
   314  			Namespace: key.Namespace,
   315  			ModelUUID: key.ModelUUID,
   316  			Lease:     key.Lease,
   317  		}] = SnapshotEntry{
   318  			Holder:   entry.holder,
   319  			Start:    entry.start,
   320  			Duration: entry.duration,
   321  		}
   322  	}
   323  
   324  	pinned := make(map[SnapshotKey][]string)
   325  	for key, entities := range f.pinned {
   326  		if entities.IsEmpty() {
   327  			continue
   328  		}
   329  		pinned[SnapshotKey{
   330  			Namespace: key.Namespace,
   331  			ModelUUID: key.ModelUUID,
   332  			Lease:     key.Lease,
   333  		}] = entities.SortedValues()
   334  	}
   335  
   336  	f.mu.Unlock()
   337  
   338  	return &Snapshot{
   339  		Version:    SnapshotVersion,
   340  		Entries:    entries,
   341  		Pinned:     pinned,
   342  		GlobalTime: f.globalTime,
   343  	}, nil
   344  }
   345  
   346  // Restore is part of raft.FSM.
   347  func (f *FSM) Restore(reader io.ReadCloser) error {
   348  	defer reader.Close()
   349  
   350  	var snapshot Snapshot
   351  	decoder := yaml.NewDecoder(reader)
   352  	if err := decoder.Decode(&snapshot); err != nil {
   353  		return errors.Trace(err)
   354  	}
   355  	if snapshot.Version != SnapshotVersion {
   356  		return errors.NotValidf("snapshot version %d", snapshot.Version)
   357  	}
   358  	if snapshot.Entries == nil {
   359  		return errors.NotValidf("nil entries")
   360  	}
   361  
   362  	newEntries := make(map[lease.Key]*entry, len(snapshot.Entries))
   363  	for key, ssEntry := range snapshot.Entries {
   364  		newEntries[lease.Key{
   365  			Namespace: key.Namespace,
   366  			ModelUUID: key.ModelUUID,
   367  			Lease:     key.Lease,
   368  		}] = &entry{
   369  			holder:   ssEntry.Holder,
   370  			start:    ssEntry.Start,
   371  			duration: ssEntry.Duration,
   372  		}
   373  	}
   374  
   375  	newPinned := make(map[lease.Key]set.Strings, len(snapshot.Pinned))
   376  	for key, entities := range snapshot.Pinned {
   377  		newPinned[lease.Key{
   378  			Namespace: key.Namespace,
   379  			ModelUUID: key.ModelUUID,
   380  			Lease:     key.Lease,
   381  		}] = set.NewStrings(entities...)
   382  	}
   383  
   384  	f.mu.Lock()
   385  	f.globalTime = snapshot.GlobalTime
   386  	f.entries = newEntries
   387  	f.pinned = newPinned
   388  	f.mu.Unlock()
   389  
   390  	return nil
   391  }
   392  
   393  // Snapshot defines the format of the FSM snapshot.
   394  type Snapshot struct {
   395  	Version    int                           `yaml:"version"`
   396  	Entries    map[SnapshotKey]SnapshotEntry `yaml:"entries"`
   397  	Pinned     map[SnapshotKey][]string      `yaml:"pinned"`
   398  	GlobalTime time.Time                     `yaml:"global-time"`
   399  }
   400  
   401  // Persist is part of raft.FSMSnapshot.
   402  func (s *Snapshot) Persist(sink raft.SnapshotSink) (err error) {
   403  	defer func() {
   404  		if err != nil {
   405  			sink.Cancel()
   406  		}
   407  	}()
   408  
   409  	encoder := yaml.NewEncoder(sink)
   410  	if err := encoder.Encode(s); err != nil {
   411  		return errors.Trace(err)
   412  	}
   413  	if err := encoder.Close(); err != nil {
   414  		return errors.Trace(err)
   415  	}
   416  	return sink.Close()
   417  }
   418  
   419  // Release is part of raft.FSMSnapshot.
   420  func (s *Snapshot) Release() {}
   421  
   422  // SnapshotKey defines the format of a lease key in a snapshot.
   423  type SnapshotKey struct {
   424  	Namespace string `yaml:"namespace"`
   425  	ModelUUID string `yaml:"model-uuid"`
   426  	Lease     string `yaml:"lease"`
   427  }
   428  
   429  // SnapshotEntry defines the format of a lease entry in a snapshot.
   430  type SnapshotEntry struct {
   431  	Holder   string        `yaml:"holder"`
   432  	Start    time.Time     `yaml:"start"`
   433  	Duration time.Duration `yaml:"duration"`
   434  }
   435  
   436  // Command captures the details of an operation to be run on the FSM.
   437  type Command struct {
   438  	// Version of the command format, in case it changes and we need
   439  	// to handle multiple formats.
   440  	Version int `yaml:"version"`
   441  
   442  	// Operation is one of claim, extend, expire or setTime.
   443  	Operation string `yaml:"operation"`
   444  
   445  	// Namespace is the kind of lease.
   446  	Namespace string `yaml:"namespace,omitempty"`
   447  
   448  	// ModelUUID identifies the model the lease belongs to.
   449  	ModelUUID string `yaml:"model-uuid,omitempty"`
   450  
   451  	// Lease is the name of the lease the command affects.
   452  	Lease string `yaml:"lease,omitempty"`
   453  
   454  	// Holder is the name of the party claiming or extending the
   455  	// lease.
   456  	Holder string `yaml:"holder,omitempty"`
   457  
   458  	// Duration is how long the lease should last.
   459  	Duration time.Duration `yaml:"duration,omitempty"`
   460  
   461  	// OldTime is the previous time for time updates (to avoid
   462  	// applying stale ones).
   463  	OldTime time.Time `yaml:"old-time,omitempty"`
   464  
   465  	// NewTime is the time to store as the global time.
   466  	NewTime time.Time `yaml:"new-time,omitempty"`
   467  
   468  	// PinEntity is a tag representing an entity concerned
   469  	// with a pin or unpin operation.
   470  	PinEntity string `yaml:"pin-entity,omitempty"`
   471  }
   472  
   473  // Validate checks that the command describes a valid state change.
   474  func (c *Command) Validate() error {
   475  	// For now there's only version 1.
   476  	if c.Version != 1 {
   477  		return errors.NotValidf("version %d", c.Version)
   478  	}
   479  	switch c.Operation {
   480  	case OperationClaim, OperationExtend:
   481  		if err := c.validateLeaseKey(); err != nil {
   482  			return err
   483  		}
   484  		if err := c.validateNoTime(); err != nil {
   485  			return err
   486  		}
   487  		if c.Holder == "" {
   488  			return errors.NotValidf("%s with empty holder", c.Operation)
   489  		}
   490  		if c.Duration == 0 {
   491  			return errors.NotValidf("%s with zero duration", c.Operation)
   492  		}
   493  		if c.PinEntity != "" {
   494  			return errors.NotValidf("%s with pin entity", c.Operation)
   495  		}
   496  	case OperationPin, OperationUnpin:
   497  		if err := c.validateLeaseKey(); err != nil {
   498  			return err
   499  		}
   500  		if err := c.validateNoTime(); err != nil {
   501  			return err
   502  		}
   503  		if c.Duration != 0 {
   504  			return errors.NotValidf("%s with duration", c.Operation)
   505  		}
   506  		if c.PinEntity == "" {
   507  			return errors.NotValidf("%s with empty pin entity", c.Operation)
   508  		}
   509  	case OperationSetTime:
   510  		// An old time of 0 is valid when starting up.
   511  		var zeroTime time.Time
   512  		if c.NewTime == zeroTime {
   513  			return errors.NotValidf("setTime with zero new time")
   514  		}
   515  		if c.Holder != "" {
   516  			return errors.NotValidf("setTime with holder")
   517  		}
   518  		if c.Duration != 0 {
   519  			return errors.NotValidf("setTime with duration")
   520  		}
   521  		if c.Namespace != "" {
   522  			return errors.NotValidf("setTime with namespace")
   523  		}
   524  		if c.ModelUUID != "" {
   525  			return errors.NotValidf("setTime with model UUID")
   526  		}
   527  		if c.Lease != "" {
   528  			if c.Holder == "" {
   529  				return errors.NotValidf("%s with empty holder", c.Operation)
   530  			}
   531  			return errors.NotValidf("setTime with lease")
   532  		}
   533  		if c.PinEntity != "" {
   534  			return errors.NotValidf("setTime with pin entity")
   535  		}
   536  	default:
   537  		return errors.NotValidf("operation %q", c.Operation)
   538  	}
   539  	return nil
   540  }
   541  
   542  func (c *Command) validateLeaseKey() error {
   543  	if c.Namespace == "" {
   544  		return errors.NotValidf("%s with empty namespace", c.Operation)
   545  	}
   546  	if c.ModelUUID == "" {
   547  		return errors.NotValidf("%s with empty model UUID", c.Operation)
   548  	}
   549  	if c.Lease == "" {
   550  		return errors.NotValidf("%s with empty lease", c.Operation)
   551  	}
   552  	return nil
   553  }
   554  
   555  func (c *Command) validateNoTime() error {
   556  	var zeroTime time.Time
   557  	if c.OldTime != zeroTime {
   558  		return errors.NotValidf("%s with old time", c.Operation)
   559  	}
   560  	if c.NewTime != zeroTime {
   561  		return errors.NotValidf("%s with new time", c.Operation)
   562  	}
   563  	return nil
   564  }
   565  
   566  // LeaseKey makes a lease key from the fields in the command.
   567  func (c *Command) LeaseKey() lease.Key {
   568  	return lease.Key{
   569  		Namespace: c.Namespace,
   570  		ModelUUID: c.ModelUUID,
   571  		Lease:     c.Lease,
   572  	}
   573  }
   574  
   575  // Marshal converts this command to a byte slice.
   576  func (c *Command) Marshal() ([]byte, error) {
   577  	return yaml.Marshal(c)
   578  }
   579  
   580  // UnmarshalCommand converts a marshalled command []byte into a
   581  // command.
   582  func UnmarshalCommand(data []byte) (*Command, error) {
   583  	var result Command
   584  	err := yaml.Unmarshal(data, &result)
   585  	return &result, err
   586  }