public async ValueTask <Dictionary <IDatabase, Task <bool> >?> TryAcquireAsync()
        {
            this._cancellationToken.ThrowIfCancellationRequested();

            var isSynchronous = SyncViaAsync.IsSynchronous;

            if (isSynchronous && this._databases.Count == 1)
            {
                return(this.TrySingleFullySynchronousAcquire());
            }

            var primitive       = this._primitive;
            var tryAcquireTasks = this._databases.ToDictionary(
                db => db,
                db => Helpers.SafeCreateTask(state => state.primitive.TryAcquireAsync(state.db), (primitive, db))
                );

            var waitForAcquireTask = this.WaitForAcquireAsync(tryAcquireTasks);

            var succeeded = false;

            try
            {
                succeeded = await waitForAcquireTask.AwaitSyncOverAsync().ConfigureAwait(false);
            }
            finally
            {
                // clean up
                if (!succeeded)
                {
                    List <Task>?releaseTasks = null;
                    foreach (var kvp in tryAcquireTasks)
                    {
                        // if the task hasn't finished yet, we don't want to do any releasing now; just
                        // queue a release command to run when the task eventually completes
                        if (!kvp.Value.IsCompleted)
                        {
                            RedLockHelper.FireAndForgetReleaseUponCompletion(primitive, kvp.Key, kvp.Value);
                        }
                        // otherwise, unless we know we failed to acquire, do a release
                        else if (!RedLockHelper.ReturnedFalse(kvp.Value))
                        {
                            if (isSynchronous)
                            {
                                try { primitive.Release(kvp.Key, fireAndForget: true); }
                                catch { }
                            }
                            else
                            {
                                (releaseTasks ??= new List <Task>())
                                .Add(Helpers.SafeCreateTask(state => state.primitive.ReleaseAsync(state.Key, fireAndForget: true), (primitive, kvp.Key)));
                            }
                        }
                    }

                    if (releaseTasks != null)
                    {
                        await Task.WhenAll(releaseTasks).ConfigureAwait(false);
                    }
                }
            }

            return(succeeded ? tryAcquireTasks : null);
        }
Esempio n. 2
0
        public async Task <bool?> TryExtendAsync()
        {
            Invariant.Require(!SyncViaAsync.IsSynchronous, "should only be called from a background renewal thread which is async");

            var incompleteTasks = new HashSet <Task>();

            foreach (var kvp in this._tryAcquireOrRenewTasks.ToArray())
            {
                if (kvp.Value.IsCompleted)
                {
                    incompleteTasks.Add(
                        this._tryAcquireOrRenewTasks[kvp.Key] = Helpers.SafeCreateTask(
                            state => state.primitive.TryExtendAsync(state.database),
                            (primitive: this._primitive, database: kvp.Key)
                            )
                        );
                }
                else
                {
                    // if the previous acquire/renew is still going, just keep waiting for that
                    incompleteTasks.Add(kvp.Value);
                }
            }

            // For extension we use the same timeout as acquire. This ensures the same min validity time which should be
            // sufficient to keep extending
            using var timeout = new TimeoutTask(this._primitive.AcquireTimeout, this._cancellationToken);
            incompleteTasks.Add(timeout.Task);

            var databaseCount = this._tryAcquireOrRenewTasks.Count;
            var successCount  = 0;
            var failCount     = 0;

            while (true)
            {
                var completed = await Task.WhenAny(incompleteTasks).ConfigureAwait(false);

                if (completed == timeout.Task)
                {
                    await completed.ConfigureAwait(false); // propagate cancellation

                    return(null);                          // inconclusive
                }

                if (completed.Status == TaskStatus.RanToCompletion && ((Task <bool>)completed).Result)
                {
                    ++successCount;
                    if (RedLockHelper.HasSufficientSuccesses(successCount, databaseCount))
                    {
                        return(true);
                    }
                }
                else
                {
                    // note that we treat faulted and failed the same in extend. There's no reason to throw, since
                    // this is just called by the extend loop. While in theory a fault could indicate some kind of post-success
                    // failure, most likely it means the db is unreachable and so it is safest to consider it a failure
                    ++failCount;
                    if (RedLockHelper.HasTooManyFailuresOrFaults(failCount, databaseCount))
                    {
                        return(false);
                    }
                }

                incompleteTasks.Remove(completed);
            }
        }
        private async Task <bool> WaitForAcquireAsync(IReadOnlyDictionary <IDatabase, Task <bool> > tryAcquireTasks)
        {
            using var timeout = new TimeoutTask(this._primitive.AcquireTimeout, this._cancellationToken);
            var incompleteTasks = new HashSet <Task>(tryAcquireTasks.Values)
            {
                timeout.Task
            };

            var successCount = 0;
            var failCount    = 0;
            var faultCount   = 0;

            while (true)
            {
                var completed = await Task.WhenAny(incompleteTasks).ConfigureAwait(false);

                if (completed == timeout.Task)
                {
                    await completed.ConfigureAwait(false); // propagates cancellation

                    return(false);                         // true timeout
                }

                if (completed.Status == TaskStatus.RanToCompletion)
                {
                    var result = await((Task <bool>)completed).ConfigureAwait(false);
                    if (result)
                    {
                        ++successCount;
                        if (RedLockHelper.HasSufficientSuccesses(successCount, this._databases.Count))
                        {
                            return(true);
                        }
                    }
                    else
                    {
                        ++failCount;
                        if (RedLockHelper.HasTooManyFailuresOrFaults(failCount, this._databases.Count))
                        {
                            return(false);
                        }
                    }
                }
                else // faulted or canceled
                {
                    // if we get too many faults, the lock is not possible to acquire, so we should throw
                    ++faultCount;
                    if (RedLockHelper.HasTooManyFailuresOrFaults(faultCount, this._databases.Count))
                    {
                        var faultingTasks = tryAcquireTasks.Values.Where(t => t.IsCanceled || t.IsFaulted)
                                            .ToArray();
                        if (faultingTasks.Length == 1)
                        {
                            await faultingTasks[0].ConfigureAwait(false); // propagate the error
                        }

                        throw new AggregateException(faultingTasks.Select(t => t.Exception ?? new TaskCanceledException(t).As <Exception>()))
                              .Flatten();
                    }

                    ++failCount;
                    if (RedLockHelper.HasTooManyFailuresOrFaults(failCount, this._databases.Count))
                    {
                        return(false);
                    }
                }

                incompleteTasks.Remove(completed);
                Invariant.Require(incompleteTasks.Count > 1, "should be more than just timeout left");
            }
        }
Esempio n. 4
0
        public async ValueTask ReleaseAsync()
        {
            var isSynchronous = SyncViaAsync.IsSynchronous;
            var unreleasedTryAcquireOrRenewTasks = this._tryAcquireOrRenewTasks.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);

            List <Exception>?releaseExceptions = null;
            var successCount  = 0;
            var faultCount    = 0;
            var databaseCount = unreleasedTryAcquireOrRenewTasks.Count;

            try
            {
                while (true)
                {
                    var releaseableDatabases = unreleasedTryAcquireOrRenewTasks.Where(kvp => kvp.Value.IsCompleted)
                                               // work through non-faulted tasks first
                                               .OrderByDescending(kvp => kvp.Value.Status == TaskStatus.RanToCompletion)
                                               // then start with failed since no action is required to release those
                                               .ThenBy(kvp => kvp.Value.Status == TaskStatus.RanToCompletion && kvp.Value.Result)
                                               .Select(kvp => kvp.Key)
                                               .ToArray();
                    foreach (var db in releaseableDatabases)
                    {
                        var tryAcquireOrRenewTask = unreleasedTryAcquireOrRenewTasks[db];
                        unreleasedTryAcquireOrRenewTasks.Remove(db);

                        if (RedLockHelper.ReturnedFalse(tryAcquireOrRenewTask))
                        {
                            // if we failed to acquire, we don't need to release
                            ++successCount;
                        }
                        else
                        {
                            try
                            {
                                if (isSynchronous)
                                {
                                    this._primitive.Release(db, fireAndForget: false);
                                }
                                else
                                {
                                    await this._primitive.ReleaseAsync(db, fireAndForget : false).ConfigureAwait(false);
                                }
                                ++successCount;
                            }
                            catch (Exception ex)
                            {
                                (releaseExceptions ??= new List <Exception>()).Add(ex);
                                ++faultCount;
                                if (RedLockHelper.HasTooManyFailuresOrFaults(faultCount, databaseCount))
                                {
                                    throw new AggregateException(releaseExceptions !).Flatten();
                                }
                            }
                        }

                        if (RedLockHelper.HasSufficientSuccesses(successCount, databaseCount))
                        {
                            return;
                        }
                    }

                    // if we haven't released enough yet to be done or certain of success or failure, wait for another to finish
                    if (isSynchronous)
                    {
                        Task.WaitAny(unreleasedTryAcquireOrRenewTasks.Values.ToArray());
                    }
                    else
                    {
                        await Task.WhenAny(unreleasedTryAcquireOrRenewTasks.Values).ConfigureAwait(false);
                    }
                }
            }
            finally // fire and forget the rest
            {
                foreach (var kvp in unreleasedTryAcquireOrRenewTasks)
                {
                    RedLockHelper.FireAndForgetReleaseUponCompletion(this._primitive, kvp.Key, kvp.Value);
                }
            }
        }