github.com/mailgun/holster/v4@v4.20.0/consul/lock.go (about)

     1  package consul
     2  
     3  import (
     4  	"context"
     5  	"sync"
     6  	"time"
     7  
     8  	"github.com/hashicorp/consul/api"
     9  	"github.com/mailgun/holster/v4/errors"
    10  	"github.com/mailgun/holster/v4/setter"
    11  	"github.com/mailgun/holster/v4/syncutil"
    12  	"github.com/sirupsen/logrus"
    13  )
    14  
    15  type lock struct {
    16  	wg     syncutil.WaitGroup
    17  	cfg    *LockConfig
    18  	mutex  sync.Mutex
    19  	locked bool
    20  }
    21  
    22  type LockConfig struct {
    23  	Client      *api.Client
    24  	LockOptions *api.LockOptions
    25  	Log         *logrus.Entry
    26  	OnChange    func(bool)
    27  }
    28  
    29  // Lock attempts to get a lock then continues to keep the lock until told to stop
    30  type Lock interface {
    31  	PutValue(ctx context.Context, b []byte) error
    32  	Unlock(b []byte)
    33  	HasLock() bool
    34  }
    35  
    36  // SpawnLock spawns a goroutine to handle lock life cycle. Blocks until the lock is acquired,
    37  // or the context is cancelled. Returns a lock that holds the current state of the lock
    38  func SpawnLock(ctx context.Context, cfg *LockConfig) (*lock, error) {
    39  	l := &lock{
    40  		cfg: cfg,
    41  	}
    42  
    43  	setter.SetDefault(&l.cfg.Log, logrus.WithField("category", "consul-lock"))
    44  
    45  	// Start acquire lock loop
    46  	errCh := l.spawn(cfg.Client, cfg.LockOptions)
    47  
    48  	select {
    49  	case <-errCh:
    50  		return l, nil
    51  	case <-ctx.Done():
    52  		return nil, errors.Wrapf(ctx.Err(), "while waiting for initial lock on '%s'", cfg.LockOptions.Key)
    53  	}
    54  }
    55  
    56  func (l *lock) spawn(c *api.Client, opts *api.LockOptions) chan error {
    57  	l.cfg.Log = l.cfg.Log.WithField("lock-name", opts.Key)
    58  	errorCh := make(chan error)
    59  
    60  	l.wg.Until(func(done chan struct{}) bool {
    61  		running := true
    62  
    63  		// Case where we are looping trying to acquire the
    64  		// lock again but are asked to shutdown
    65  		select {
    66  		case <-done:
    67  			return false
    68  		default:
    69  		}
    70  
    71  		// Will only error on invalid config
    72  		lock, err := c.LockOpts(opts)
    73  		if err != nil {
    74  			errorCh <- errors.Wrap(err, "while creating lock")
    75  			return false
    76  		}
    77  
    78  		l.cfg.Log.Debug("acquiring lock")
    79  		lockCh, err := lock.Lock(nil)
    80  		if lockCh == nil {
    81  			if err == nil {
    82  				l.cfg.Log.Warn("timeout during lock acquisition; retrying")
    83  				goto RETRY
    84  			}
    85  			l.cfg.Log.WithError(err).Warn("lock acquisition failed; retrying")
    86  			time.Sleep(time.Second)
    87  			goto RETRY
    88  		}
    89  
    90  		select {
    91  		case <-lockCh:
    92  			l.cfg.Log.Warn("failed Lock acquisition; another instance trying to claim the lock?; retrying")
    93  			time.Sleep(time.Second)
    94  			goto RETRY
    95  		default:
    96  		}
    97  
    98  		l.setLocked(true)
    99  		// We have lock, notify if someone is listening
   100  		select {
   101  		case errorCh <- nil:
   102  		default:
   103  		}
   104  
   105  		// Wait for lock to be lost
   106  		select {
   107  		case <-lockCh:
   108  			l.cfg.Log.Warn("lock lost; retrying")
   109  			// Log lock was lost
   110  		case <-done:
   111  			running = false
   112  		}
   113  
   114  	RETRY:
   115  		// Release ownership of the lock and cancel the session
   116  		l.cfg.Log.Debug("releasing lock")
   117  		if err := lock.Unlock(); err != nil {
   118  			l.cfg.Log.WithError(err).Warn("while unlocking")
   119  		}
   120  		l.setLocked(false)
   121  
   122  		// If we are in shutdown
   123  		if !running {
   124  			if l.cfg.LockOptions.SessionOpts != nil &&
   125  				l.cfg.LockOptions.SessionOpts.Behavior == api.SessionBehaviorDelete {
   126  				if err := lock.Destroy(); err != nil {
   127  					l.cfg.Log.WithError(err).Warn("during lock destroy")
   128  				}
   129  			}
   130  		}
   131  		return running
   132  	})
   133  	return errorCh
   134  }
   135  
   136  func (l *lock) setLocked(s bool) {
   137  	l.mutex.Lock()
   138  	defer l.mutex.Unlock()
   139  	l.cfg.Log.Debugf("Set Lock %t", s)
   140  	if l.cfg.OnChange != nil {
   141  		if l.locked != s {
   142  			l.cfg.OnChange(s)
   143  		}
   144  	}
   145  	l.locked = s
   146  }
   147  
   148  func (l *lock) HasLock() bool {
   149  	l.mutex.Lock()
   150  	defer l.mutex.Unlock()
   151  	return l.locked
   152  }
   153  
   154  // PutValue stores the given byte slice into the value of the locked key in consul
   155  // returns error if the put failed, also updates the value that will be saved
   156  // when `Unlock()` is called.
   157  func (l *lock) PutValue(ctx context.Context, b []byte) error {
   158  	l.cfg.LockOptions.Value = b
   159  	_, err := l.cfg.Client.KV().Put(&api.KVPair{
   160  		Key:   l.cfg.LockOptions.Key,
   161  		Value: b,
   162  	}, new(api.WriteOptions).WithContext(ctx))
   163  	if err != nil {
   164  		return errors.Wrap(err, "during put for release")
   165  	}
   166  	return nil
   167  }
   168  
   169  // Unlock cancels the lock and closes any running goroutines.
   170  func (l *lock) Unlock(b []byte) {
   171  	l.cfg.Log.Infof("Unlock(%s)\n", string(b))
   172  	if b != nil {
   173  		l.cfg.LockOptions.Value = b
   174  	}
   175  	l.wg.Stop()
   176  }
   177  
   178  type Mock struct{}
   179  
   180  func (*Mock) PutValue(ctx context.Context, b []byte) error { return nil }
   181  func (*Mock) Unlock(b []byte)                              {}
   182  func (*Mock) HasLock() bool                                { return true }