Esempio n. 1
0
        /// <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.
                        }
                    }
                }
            }
        }
Esempio n. 2
0
        /// <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.
                        }
                    }
                }
            }
        }