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 }