/// <summary> /// The internal lock extender. /// </summary> /// <returns></returns> private async Task LockExtender() { while (!_disposeCancellation.IsCancellationRequested) { // wait await Task.Delay(_refreshSpan).ConfigureAwait(false); // get expiry time TimeSpan expiryTime = _expirySpan; // gather all locks that need to be refreshed or expired RedisLockHandle[] refreshableLocks = null; IEnumerable <RedisLockHandle> expiredLocks = null; lock (_handles) { refreshableLocks = _handles.Where(h => h.RefreshedAt <(DateTime.UtcNow - TimeSpan.FromSeconds(30)) || h.ExpiresAt> DateTime.UtcNow).ToArray(); expiredLocks = _handles.Where(h => h.ExpiresAt < DateTime.UtcNow); // process any expires locks foreach (RedisLockHandle handle in expiredLocks) { _handles.Remove(handle); } } // build refresh lock tasks List <Task <bool> > refreshLockTasks = new List <Task <bool> >(refreshableLocks.Length); Dictionary <Task <bool>, RedisLockHandle> refreshLockHandles = new Dictionary <Task <bool>, RedisLockHandle>(); Dictionary <Task <bool>, DateTime> refreshLockTimes = new Dictionary <Task <bool>, DateTime>(); foreach (RedisLockHandle handle in refreshableLocks) { // create task Task <bool> task = _database.LockExtendAsync($"tandem.{handle.ResourceURI.ToString()}", handle.Token.ToString(), expiryTime); // add and map refreshLockTasks.Add(task); refreshLockHandles[task] = handle; refreshLockTimes[task] = DateTime.UtcNow; } // refresh all locks try { await Task.WhenAll(refreshLockTasks).ConfigureAwait(false); } catch (Exception) { } // map any sucesses List <RedisLockHandle> refreshFailures = new List <RedisLockHandle>(); foreach (var task in refreshLockTasks) { // find associated handle RedisLockHandle handle = refreshLockHandles[task]; DateTime time = refreshLockTimes[task]; // ignore faulty refreshes, if it expires eventually we'll catch that too if (task.IsFaulted) { // invoke event try { OnInvalidated(new RedisLockInvalidatedEventArgs() { Handle = handle }); } catch (Exception) { } continue; } // check if we successfully refreshed the handle if (task.Result == true) { handle.RefreshedAt = time; handle.ExpiresAt = time + expiryTime; } else { refreshFailures.Add(handle); } } lock (_handles) { foreach (var handle in refreshFailures) { try { _handles.Remove(handle); } catch (Exception) { } } } } }
/// <summary> /// Locks the specified resource. /// </summary> /// <param name="resourceUri">The resource URI.</param> /// <param name="waitTime">The maximum amount of time to wait.</param> /// <param name="cancellationToken">The cancellation token.</param> /// <returns>The lock handle or null if the lock could not be obtained.</returns> public async Task <ILockHandle> LockAsync(Uri resourceUri, TimeSpan waitTime = default(TimeSpan), CancellationToken cancellationToken = default(CancellationToken)) { if (_disposed == 1) { throw new ObjectDisposedException("The lock manager has been disposed"); } if (!resourceUri.Scheme.Equals("tandem", StringComparison.CurrentCultureIgnoreCase)) { throw new FormatException("The protocol scheme must be tandem"); } // generate random token LockToken token = new LockToken(Guid.NewGuid(), _owner); // get remaining time TimeSpan remainingTime = waitTime; while (remainingTime >= TimeSpan.Zero) { // check if cancelled cancellationToken.ThrowIfCancellationRequested(); // try and get the lock bool gotLock = await _database.LockTakeAsync($"tandem.{resourceUri.ToString()}", token.ToString(), _expirySpan).ConfigureAwait(false); if (gotLock) { // create handle RedisLockHandle handle = new RedisLockHandle(this) { ResourceURI = resourceUri, Token = token }; // add handle lock (_handles) { _handles.Add(handle); } return(handle); } else { if (waitTime == TimeSpan.Zero) { return(null); } if (remainingTime > TimeSpan.FromMilliseconds(3000)) { // no point waiting anymore, no cigar for the lock return(null); } else { // wait 3 seconds until we try again await Task.Delay(3000, cancellationToken).ConfigureAwait(false); remainingTime -= TimeSpan.FromSeconds(3); } } } return(null); }