// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using StackExchange.Redis; namespace EpicGames.Redis.Utility { /// /// Implements a named single-entry lock which expires after a period of time if the process terminates. /// public class RedisLock : IAsyncDisposable, IDisposable { readonly IDatabase _database; readonly RedisKey _key; CancellationTokenSource? _cancellationSource; Task? _backgroundTask; /// /// Constructor /// /// /// public RedisLock(IDatabase database, RedisKey key) { _database = database; _key = key; } /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Dispose pattern /// /// protected virtual void Dispose(bool disposing) { if (disposing) { DisposeAsync().AsTask().Wait(); } } /// public async ValueTask DisposeAsync() { if (_cancellationSource != null) { await _cancellationSource.CancelAsync(); } if (_backgroundTask != null) { await _backgroundTask; } _backgroundTask?.Dispose(); _cancellationSource?.Dispose(); _backgroundTask = null; _cancellationSource = null; GC.SuppressFinalize(this); } /// /// Attempts to acquire the lock for the given period of time. The lock will be renewed once half of this interval has elapsed. /// /// Time after which the lock expires /// Whether the lock should be released when disposed. If false, the lock will be held for the given time, but not renewed once disposed. /// True if the lock was acquired, false if another service already has it public async ValueTask AcquireAsync(TimeSpan duration, bool releaseOnDispose = true) { if (await _database.StringSetAsync(_key, RedisValue.EmptyString, duration, When.NotExists)) { _cancellationSource = new CancellationTokenSource(); _backgroundTask = Task.Run(() => RenewAsync(duration, releaseOnDispose, _cancellationSource.Token)); return true; } return false; } /// /// Background task which renews the lock while the service is running /// /// /// Whether the lock should be released when the cancellation token is fired /// /// async Task RenewAsync(TimeSpan duration, bool releaseOnDispose, CancellationToken cancellationToken) { Stopwatch timer = Stopwatch.StartNew(); for (; ; ) { await Task.Delay(duration / 2, cancellationToken).ContinueWith(x => { }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); // Do not throw if (cancellationToken.IsCancellationRequested) { timer.Stop(); if (releaseOnDispose || timer.Elapsed > duration) { await _database.StringSetAsync(_key, RedisValue.Null); } else { await _database.StringSetAsync(_key, RedisValue.EmptyString, duration - timer.Elapsed, When.Exists); } break; } if (!await _database.StringSetAsync(_key, RedisValue.EmptyString, duration, When.Exists)) { break; } } } } /// /// Implements a named single-entry lock which expires after a period of time if the process terminates. /// /// Type of the value identifying the lock uniqueness public class RedisLock : RedisLock { /// /// Constructor /// /// /// /// public RedisLock(IDatabase database, RedisKey baseKey, T value) : base(database, baseKey.Append(RedisSerializer.Serialize(value).AsKey())) { } } }