/// <summary> /// Lock attempts to acquire the lock and blocks while doing so. /// Providing a CancellationToken can be used to abort the lock attempt. /// There is no notification that the lock has been lost, but IsHeld may be set to False at any time due to session invalidation, communication errors, operator intervention, etc. /// It is NOT safe to assume that the lock is held until Unlock() unless the Session is specifically created without any associated health checks. /// Users of the Lock object should check the IsHeld property before entering the critical section of their code, e.g. in a "while (myLock.IsHeld) {criticalsection}" block. /// By default Consul sessions prefer liveness over safety and an application must be able to handle the lock being lost. /// </summary> /// <param name="ct">The cancellation token to cancel lock acquisition</param> public async Task <CancellationToken> Acquire(CancellationToken ct) { try { using (await _mutex.LockAsync().ConfigureAwait(false)) { if (IsHeld) { // Check if we already hold the lock throw new LockHeldException(); } // Don't overwrite the CancellationTokenSource until AFTER we've tested for holding, // since there might be tasks that are currently running for this lock. DisposeCancellationTokenSource(); _cts = new CancellationTokenSource(); // Check if we need to create a session first if (string.IsNullOrEmpty(Opts.Session)) { LockSession = await CreateSession().ConfigureAwait(false); _sessionRenewTask = _client.Session.RenewPeriodic(Opts.SessionTTL, LockSession, WriteOptions.Default, _cts.Token); } else { LockSession = Opts.Session; } var qOpts = new QueryOptions() { WaitTime = Opts.LockWaitTime }; var attempts = 0; var sw = Stopwatch.StartNew(); while (!ct.IsCancellationRequested) { if (attempts > 0 && Opts.LockTryOnce) { var elapsed = sw.Elapsed; if (elapsed > Opts.LockWaitTime) { DisposeCancellationTokenSource(); throw new LockMaxAttemptsReachedException("LockTryOnce is set and the lock is already held or lock delay is in effect"); } qOpts.WaitTime = Opts.LockWaitTime - elapsed; } attempts++; QueryResult <KVPair> pair; pair = await _client.KV.Get(Opts.Key, qOpts).ConfigureAwait(false); if (pair.Response != null) { if (pair.Response.Flags != LockFlagValue) { DisposeCancellationTokenSource(); throw new LockConflictException(); } // Already locked by this session if (pair.Response.Session == LockSession) { // Don't restart MonitorLock if this session already holds the lock if (IsHeld) { return(_cts.Token); } IsHeld = true; _monitorTask = MonitorLock(); return(_cts.Token); } // If it's not empty, some other session must have the lock if (!string.IsNullOrEmpty(pair.Response.Session)) { qOpts.WaitIndex = pair.LastIndex; continue; } } // If the code executes this far, no other session has the lock, so try to lock it var kvPair = LockEntry(LockSession); var locked = (await _client.KV.Acquire(kvPair).ConfigureAwait(false)).Response; // KV acquisition succeeded, so the session now holds the lock if (locked) { IsHeld = true; _monitorTask = MonitorLock(); return(_cts.Token); } // Handle the case of not getting the lock if (ct.IsCancellationRequested) { DisposeCancellationTokenSource(); throw new TaskCanceledException(); } // Failed to get the lock, determine why by querying for the key again qOpts.WaitIndex = 0; pair = await _client.KV.Get(Opts.Key, qOpts).ConfigureAwait(false); // If the session is not null, this means that a wait can safely happen using a long poll if (pair.Response != null && pair.Response.Session != null) { qOpts.WaitIndex = pair.LastIndex; continue; } // If the session is null and the lock failed to acquire, then it means // a lock-delay is in effect and a timed wait must be used to avoid a hot loop. try { await Task.Delay(Opts.LockRetryTime, ct).ConfigureAwait(false); } catch (TaskCanceledException) { /* Ignore TaskTaskCanceledException */ } } DisposeCancellationTokenSource(); throw new LockNotHeldException("Unable to acquire the lock with Consul"); } } finally { if (ct.IsCancellationRequested || (!IsHeld && !string.IsNullOrEmpty(Opts.Session))) { DisposeCancellationTokenSource(); if (_sessionRenewTask != null) { try { await _monitorTask.ConfigureAwait(false); await _sessionRenewTask.ConfigureAwait(false); } catch (AggregateException) { // Ignore AggregateExceptions from the tasks during Release, since if the Renew task died, the developer will be Super Confused if they see the exception during Release. } } } } }
/// <summary> /// Acquire attempts to reserve a slot in the semaphore, blocking until success, interrupted via CancellationToken or if an error is encountered. /// A provided CancellationToken can be used to abort the attempt. /// There is no notification that the semaphore slot has been lost, but IsHeld may be set to False at any time due to session invalidation, communication errors, operator intervention, etc. /// It is NOT safe to assume that the slot is held until Release() unless the Session is specifically created without any associated health checks. /// By default Consul sessions prefer liveness over safety and an application must be able to handle the session being lost. /// </summary> /// <param name="ct">The cancellation token to cancel semaphore acquisition</param> public async Task <CancellationToken> Acquire(CancellationToken ct) { try { using (await _mutex.LockAsync().ConfigureAwait(false)) { if (IsHeld) { // Check if we already hold the lock throw new SemaphoreHeldException(); } // Don't overwrite the CancellationTokenSource until AFTER we've tested for holding, since there might be tasks that are currently running for this lock. DisposeCancellationTokenSource(); _cts = new CancellationTokenSource(); // Check if we need to create a session first if (string.IsNullOrEmpty(Opts.Session)) { try { LockSession = await CreateSession().ConfigureAwait(false); _sessionRenewTask = _client.Session.RenewPeriodic(Opts.SessionTTL, LockSession, WriteOptions.Default, _cts.Token); } catch (Exception ex) { DisposeCancellationTokenSource(); throw new InvalidOperationException("Failed to create session", ex); } } else { LockSession = Opts.Session; } var contender = (await _client.KV.Acquire(ContenderEntry(LockSession)).ConfigureAwait(false)).Response; if (!contender) { DisposeCancellationTokenSource(); throw new KeyNotFoundException("Failed to make contender entry"); } var qOpts = new QueryOptions() { WaitTime = Opts.SemaphoreWaitTime }; var attempts = 0; var start = DateTime.UtcNow; while (!ct.IsCancellationRequested) { if (attempts > 0 && Opts.SemaphoreTryOnce) { var elapsed = DateTime.UtcNow.Subtract(start); if (elapsed > qOpts.WaitTime) { DisposeCancellationTokenSource(); throw new SemaphoreMaxAttemptsReachedException("SemaphoreTryOnce is set and the semaphore is already at maximum capacity"); } qOpts.WaitTime -= elapsed; } attempts++; QueryResult <KVPair[]> pairs; try { pairs = await _client.KV.List(Opts.Prefix, qOpts).ConfigureAwait(false); } catch (Exception ex) { DisposeCancellationTokenSource(); throw new KeyNotFoundException("Failed to read prefix", ex); } var lockPair = FindLock(pairs.Response); if (lockPair.Flags != SemaphoreFlagValue) { DisposeCancellationTokenSource(); throw new SemaphoreConflictException(); } var semaphoreLock = DecodeLock(lockPair); if (semaphoreLock.Limit != Opts.Limit) { DisposeCancellationTokenSource(); throw new SemaphoreLimitConflictException( string.Format("Semaphore limit conflict (lock: {0}, local: {1})", semaphoreLock.Limit, Opts.Limit), semaphoreLock.Limit, Opts.Limit); } PruneDeadHolders(semaphoreLock, pairs.Response); if (semaphoreLock.Holders.Count >= semaphoreLock.Limit) { qOpts.WaitIndex = pairs.LastIndex; continue; } semaphoreLock.Holders[LockSession] = true; var newLock = EncodeLock(semaphoreLock, lockPair.ModifyIndex); if (ct.IsCancellationRequested) { DisposeCancellationTokenSource(); throw new TaskCanceledException(); } // Handle the case of not getting the lock if (!(await _client.KV.CAS(newLock).ConfigureAwait(false)).Response) { continue; } IsHeld = true; _monitorTask = MonitorLock(LockSession); return(_cts.Token); } DisposeCancellationTokenSource(); throw new SemaphoreNotHeldException("Unable to acquire the semaphore with Consul"); } } finally { if (ct.IsCancellationRequested || (!IsHeld && !string.IsNullOrEmpty(Opts.Session))) { DisposeCancellationTokenSource(); await _client.KV.Delete(ContenderEntry(LockSession).Key).ConfigureAwait(false); if (_sessionRenewTask != null) { try { await _monitorTask.ConfigureAwait(false); await _sessionRenewTask.ConfigureAwait(false); } catch (Exception) { // Ignore Exceptions from the tasks during Release, since if the Renew task died, the developer will be Super Confused if they see the exception during Release. } } } } }