public async Task <T> LockAsync <T>(Key key, Func <CancellationToken, Task <T> > callback, CancellationToken cancellationToken) { if (callback == null) { throw new ArgumentNullException(nameof(callback)); } using var state = new LockState(key, _identifierGenerator.GetUniqueKey(15), cancellationToken); try { await _scriptLibrary.SubscribeAsync(state).ConfigureAwait(false); try { if (await _scriptLibrary.GetLockOrAddToQueue(state.Parameters).ConfigureAwait(false)) { state.SetWithKey(); } //we start protecting after the first manipulation in redis, to properly hit the expiration if needed using (var protector = new LockProtector(_scriptLibrary, state)) { await state.WaitingTask.ConfigureAwait(false); if (state.Token.IsCancellationRequested) { throw new TaskCanceledException(); } return(await callback(state.Token).ConfigureAwait(false)); } } finally { await _scriptLibrary.FreeLockAndPop(state.Parameters).ConfigureAwait(false); } } finally { await _scriptLibrary.UnSubscribeAsync(state).ConfigureAwait(false); } }
//for testing purposes internal async Task EnsureNoDeadLockAsync() { if (_state.State == State.Done) { return; } //this will be repeated until disposed while (true) { try { //we wait for the time it takes the key to expire await Task.Delay(_state.Key.RedisKeyExpiration, _state.Token).ConfigureAwait(false); } catch (TaskCanceledException) { //if the token gets canceled everything went well return; } if (_state.State == State.Done) { return; } try { //let's see what's in redis at this point var result = await _scriptLibrary.GetKeySituation(_state.Parameters).ConfigureAwait(false); switch (result) { //lock in waiting list case 0: //we keep waiting break; //we owns the lock case 1: //this means the task has been completed while we were looking //we set the result and keep protecting the callback _state.SetWithKey(); break; //lock is not found case 2: switch (_state.State) { case State.WaitingForKey: //keys have expired, probably due to remote failure, we need to restart the lock process if (await _scriptLibrary.GetLockOrAddToQueue(_state.Parameters).ConfigureAwait(false)) { _state.SetWithKey(); } break; case State.WithKey: //we thought we had the key but didn't //we stop everything _state.SetDone(); return; //no need to wait for delay cancellation case State.Done: return; } break; default: _state.SetDone(new NotImplementedException("Unexpected Redis state")); return; } } catch (Exception e) { //Redis failed, we need to stop waiting for the notification _state.SetDone(e); return; } } }