github.imxd.top/hashicorp/consul@v1.4.5/api/lock.go (about) 1 package api 2 3 import ( 4 "fmt" 5 "sync" 6 "time" 7 ) 8 9 const ( 10 // DefaultLockSessionName is the Session Name we assign if none is provided 11 DefaultLockSessionName = "Consul API Lock" 12 13 // DefaultLockSessionTTL is the default session TTL if no Session is provided 14 // when creating a new Lock. This is used because we do not have another 15 // other check to depend upon. 16 DefaultLockSessionTTL = "15s" 17 18 // DefaultLockWaitTime is how long we block for at a time to check if lock 19 // acquisition is possible. This affects the minimum time it takes to cancel 20 // a Lock acquisition. 21 DefaultLockWaitTime = 15 * time.Second 22 23 // DefaultLockRetryTime is how long we wait after a failed lock acquisition 24 // before attempting to do the lock again. This is so that once a lock-delay 25 // is in effect, we do not hot loop retrying the acquisition. 26 DefaultLockRetryTime = 5 * time.Second 27 28 // DefaultMonitorRetryTime is how long we wait after a failed monitor check 29 // of a lock (500 response code). This allows the monitor to ride out brief 30 // periods of unavailability, subject to the MonitorRetries setting in the 31 // lock options which is by default set to 0, disabling this feature. This 32 // affects locks and semaphores. 33 DefaultMonitorRetryTime = 2 * time.Second 34 35 // LockFlagValue is a magic flag we set to indicate a key 36 // is being used for a lock. It is used to detect a potential 37 // conflict with a semaphore. 38 LockFlagValue = 0x2ddccbc058a50c18 39 ) 40 41 var ( 42 // ErrLockHeld is returned if we attempt to double lock 43 ErrLockHeld = fmt.Errorf("Lock already held") 44 45 // ErrLockNotHeld is returned if we attempt to unlock a lock 46 // that we do not hold. 47 ErrLockNotHeld = fmt.Errorf("Lock not held") 48 49 // ErrLockInUse is returned if we attempt to destroy a lock 50 // that is in use. 51 ErrLockInUse = fmt.Errorf("Lock in use") 52 53 // ErrLockConflict is returned if the flags on a key 54 // used for a lock do not match expectation 55 ErrLockConflict = fmt.Errorf("Existing key does not match lock use") 56 ) 57 58 // Lock is used to implement client-side leader election. It is follows the 59 // algorithm as described here: https://www.consul.io/docs/guides/leader-election.html. 60 type Lock struct { 61 c *Client 62 opts *LockOptions 63 64 isHeld bool 65 sessionRenew chan struct{} 66 lockSession string 67 l sync.Mutex 68 } 69 70 // LockOptions is used to parameterize the Lock behavior. 71 type LockOptions struct { 72 Key string // Must be set and have write permissions 73 Value []byte // Optional, value to associate with the lock 74 Session string // Optional, created if not specified 75 SessionOpts *SessionEntry // Optional, options to use when creating a session 76 SessionName string // Optional, defaults to DefaultLockSessionName (ignored if SessionOpts is given) 77 SessionTTL string // Optional, defaults to DefaultLockSessionTTL (ignored if SessionOpts is given) 78 MonitorRetries int // Optional, defaults to 0 which means no retries 79 MonitorRetryTime time.Duration // Optional, defaults to DefaultMonitorRetryTime 80 LockWaitTime time.Duration // Optional, defaults to DefaultLockWaitTime 81 LockTryOnce bool // Optional, defaults to false which means try forever 82 } 83 84 // LockKey returns a handle to a lock struct which can be used 85 // to acquire and release the mutex. The key used must have 86 // write permissions. 87 func (c *Client) LockKey(key string) (*Lock, error) { 88 opts := &LockOptions{ 89 Key: key, 90 } 91 return c.LockOpts(opts) 92 } 93 94 // LockOpts returns a handle to a lock struct which can be used 95 // to acquire and release the mutex. The key used must have 96 // write permissions. 97 func (c *Client) LockOpts(opts *LockOptions) (*Lock, error) { 98 if opts.Key == "" { 99 return nil, fmt.Errorf("missing key") 100 } 101 if opts.SessionName == "" { 102 opts.SessionName = DefaultLockSessionName 103 } 104 if opts.SessionTTL == "" { 105 opts.SessionTTL = DefaultLockSessionTTL 106 } else { 107 if _, err := time.ParseDuration(opts.SessionTTL); err != nil { 108 return nil, fmt.Errorf("invalid SessionTTL: %v", err) 109 } 110 } 111 if opts.MonitorRetryTime == 0 { 112 opts.MonitorRetryTime = DefaultMonitorRetryTime 113 } 114 if opts.LockWaitTime == 0 { 115 opts.LockWaitTime = DefaultLockWaitTime 116 } 117 l := &Lock{ 118 c: c, 119 opts: opts, 120 } 121 return l, nil 122 } 123 124 // Lock attempts to acquire the lock and blocks while doing so. 125 // Providing a non-nil stopCh can be used to abort the lock attempt. 126 // Returns a channel that is closed if our lock is lost or an error. 127 // This channel could be closed at any time due to session invalidation, 128 // communication errors, operator intervention, etc. It is NOT safe to 129 // assume that the lock is held until Unlock() unless the Session is specifically 130 // created without any associated health checks. By default Consul sessions 131 // prefer liveness over safety and an application must be able to handle 132 // the lock being lost. 133 func (l *Lock) Lock(stopCh <-chan struct{}) (<-chan struct{}, error) { 134 // Hold the lock as we try to acquire 135 l.l.Lock() 136 defer l.l.Unlock() 137 138 // Check if we already hold the lock 139 if l.isHeld { 140 return nil, ErrLockHeld 141 } 142 143 // Check if we need to create a session first 144 l.lockSession = l.opts.Session 145 if l.lockSession == "" { 146 s, err := l.createSession() 147 if err != nil { 148 return nil, fmt.Errorf("failed to create session: %v", err) 149 } 150 151 l.sessionRenew = make(chan struct{}) 152 l.lockSession = s 153 session := l.c.Session() 154 go session.RenewPeriodic(l.opts.SessionTTL, s, nil, l.sessionRenew) 155 156 // If we fail to acquire the lock, cleanup the session 157 defer func() { 158 if !l.isHeld { 159 close(l.sessionRenew) 160 l.sessionRenew = nil 161 } 162 }() 163 } 164 165 // Setup the query options 166 kv := l.c.KV() 167 qOpts := &QueryOptions{ 168 WaitTime: l.opts.LockWaitTime, 169 } 170 171 start := time.Now() 172 attempts := 0 173 WAIT: 174 // Check if we should quit 175 select { 176 case <-stopCh: 177 return nil, nil 178 default: 179 } 180 181 // Handle the one-shot mode. 182 if l.opts.LockTryOnce && attempts > 0 { 183 elapsed := time.Since(start) 184 if elapsed > l.opts.LockWaitTime { 185 return nil, nil 186 } 187 188 // Query wait time should not exceed the lock wait time 189 qOpts.WaitTime = l.opts.LockWaitTime - elapsed 190 } 191 attempts++ 192 193 // Look for an existing lock, blocking until not taken 194 pair, meta, err := kv.Get(l.opts.Key, qOpts) 195 if err != nil { 196 return nil, fmt.Errorf("failed to read lock: %v", err) 197 } 198 if pair != nil && pair.Flags != LockFlagValue { 199 return nil, ErrLockConflict 200 } 201 locked := false 202 if pair != nil && pair.Session == l.lockSession { 203 goto HELD 204 } 205 if pair != nil && pair.Session != "" { 206 qOpts.WaitIndex = meta.LastIndex 207 goto WAIT 208 } 209 210 // Try to acquire the lock 211 pair = l.lockEntry(l.lockSession) 212 locked, _, err = kv.Acquire(pair, nil) 213 if err != nil { 214 return nil, fmt.Errorf("failed to acquire lock: %v", err) 215 } 216 217 // Handle the case of not getting the lock 218 if !locked { 219 // Determine why the lock failed 220 qOpts.WaitIndex = 0 221 pair, meta, err = kv.Get(l.opts.Key, qOpts) 222 if pair != nil && pair.Session != "" { 223 //If the session is not null, this means that a wait can safely happen 224 //using a long poll 225 qOpts.WaitIndex = meta.LastIndex 226 goto WAIT 227 } else { 228 // If the session is empty and the lock failed to acquire, then it means 229 // a lock-delay is in effect and a timed wait must be used 230 select { 231 case <-time.After(DefaultLockRetryTime): 232 goto WAIT 233 case <-stopCh: 234 return nil, nil 235 } 236 } 237 } 238 239 HELD: 240 // Watch to ensure we maintain leadership 241 leaderCh := make(chan struct{}) 242 go l.monitorLock(l.lockSession, leaderCh) 243 244 // Set that we own the lock 245 l.isHeld = true 246 247 // Locked! All done 248 return leaderCh, nil 249 } 250 251 // Unlock released the lock. It is an error to call this 252 // if the lock is not currently held. 253 func (l *Lock) Unlock() error { 254 // Hold the lock as we try to release 255 l.l.Lock() 256 defer l.l.Unlock() 257 258 // Ensure the lock is actually held 259 if !l.isHeld { 260 return ErrLockNotHeld 261 } 262 263 // Set that we no longer own the lock 264 l.isHeld = false 265 266 // Stop the session renew 267 if l.sessionRenew != nil { 268 defer func() { 269 close(l.sessionRenew) 270 l.sessionRenew = nil 271 }() 272 } 273 274 // Get the lock entry, and clear the lock session 275 lockEnt := l.lockEntry(l.lockSession) 276 l.lockSession = "" 277 278 // Release the lock explicitly 279 kv := l.c.KV() 280 _, _, err := kv.Release(lockEnt, nil) 281 if err != nil { 282 return fmt.Errorf("failed to release lock: %v", err) 283 } 284 return nil 285 } 286 287 // Destroy is used to cleanup the lock entry. It is not necessary 288 // to invoke. It will fail if the lock is in use. 289 func (l *Lock) Destroy() error { 290 // Hold the lock as we try to release 291 l.l.Lock() 292 defer l.l.Unlock() 293 294 // Check if we already hold the lock 295 if l.isHeld { 296 return ErrLockHeld 297 } 298 299 // Look for an existing lock 300 kv := l.c.KV() 301 pair, _, err := kv.Get(l.opts.Key, nil) 302 if err != nil { 303 return fmt.Errorf("failed to read lock: %v", err) 304 } 305 306 // Nothing to do if the lock does not exist 307 if pair == nil { 308 return nil 309 } 310 311 // Check for possible flag conflict 312 if pair.Flags != LockFlagValue { 313 return ErrLockConflict 314 } 315 316 // Check if it is in use 317 if pair.Session != "" { 318 return ErrLockInUse 319 } 320 321 // Attempt the delete 322 didRemove, _, err := kv.DeleteCAS(pair, nil) 323 if err != nil { 324 return fmt.Errorf("failed to remove lock: %v", err) 325 } 326 if !didRemove { 327 return ErrLockInUse 328 } 329 return nil 330 } 331 332 // createSession is used to create a new managed session 333 func (l *Lock) createSession() (string, error) { 334 session := l.c.Session() 335 se := l.opts.SessionOpts 336 if se == nil { 337 se = &SessionEntry{ 338 Name: l.opts.SessionName, 339 TTL: l.opts.SessionTTL, 340 } 341 } 342 id, _, err := session.Create(se, nil) 343 if err != nil { 344 return "", err 345 } 346 return id, nil 347 } 348 349 // lockEntry returns a formatted KVPair for the lock 350 func (l *Lock) lockEntry(session string) *KVPair { 351 return &KVPair{ 352 Key: l.opts.Key, 353 Value: l.opts.Value, 354 Session: session, 355 Flags: LockFlagValue, 356 } 357 } 358 359 // monitorLock is a long running routine to monitor a lock ownership 360 // It closes the stopCh if we lose our leadership. 361 func (l *Lock) monitorLock(session string, stopCh chan struct{}) { 362 defer close(stopCh) 363 kv := l.c.KV() 364 opts := &QueryOptions{RequireConsistent: true} 365 WAIT: 366 retries := l.opts.MonitorRetries 367 RETRY: 368 pair, meta, err := kv.Get(l.opts.Key, opts) 369 if err != nil { 370 // If configured we can try to ride out a brief Consul unavailability 371 // by doing retries. Note that we have to attempt the retry in a non- 372 // blocking fashion so that we have a clean place to reset the retry 373 // counter if service is restored. 374 if retries > 0 && IsRetryableError(err) { 375 time.Sleep(l.opts.MonitorRetryTime) 376 retries-- 377 opts.WaitIndex = 0 378 goto RETRY 379 } 380 return 381 } 382 if pair != nil && pair.Session == session { 383 opts.WaitIndex = meta.LastIndex 384 goto WAIT 385 } 386 }