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); }
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"); } }
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); } } }