using Microsoft.Extensions.Caching.Distributed; using Newtonsoft.Json; namespace Nop.Core.Caching; public partial class DistributedCacheLocker : ILocker { #region Fields protected static readonly string _running = JsonConvert.SerializeObject(TaskStatus.Running); protected readonly IDistributedCache _distributedCache; #endregion #region Ctor public DistributedCacheLocker(IDistributedCache distributedCache) { _distributedCache = distributedCache; } #endregion #region Methods /// /// Performs some asynchronous task with exclusive lock /// /// The key we are locking on /// The time after which the lock will automatically be expired /// Asynchronous task to be performed with locking /// A task that resolves true if lock was acquired and action was performed; otherwise false public async Task PerformActionWithLockAsync(string resource, TimeSpan expirationTime, Func action) { //ensure that lock is acquired if (!string.IsNullOrEmpty(await _distributedCache.GetStringAsync(resource))) return false; try { await _distributedCache.SetStringAsync(resource, resource, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expirationTime }); await action(); return true; } finally { //release lock even if action fails await _distributedCache.RemoveAsync(resource); } } /// /// Starts a background task with "heartbeat": a status flag that will be periodically updated to signal to /// others that the task is running and stop them from starting the same task. /// /// The key of the background task /// The time after which the heartbeat key will automatically be expired. Should be longer than /// The interval at which to update the heartbeat, if required by the implementation /// Asynchronous background task to be performed /// A CancellationTokenSource for manually canceling the task /// A task that resolves true if lock was acquired and action was performed; otherwise false public async Task RunWithHeartbeatAsync(string key, TimeSpan expirationTime, TimeSpan heartbeatInterval, Func action, CancellationTokenSource cancellationTokenSource = default) { if (!string.IsNullOrEmpty(await _distributedCache.GetStringAsync(key))) return; var tokenSource = cancellationTokenSource ?? new CancellationTokenSource(); try { // run heartbeat early to minimize risk of multiple execution await _distributedCache.SetStringAsync( key, _running, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expirationTime }, token: tokenSource.Token); await using var timer = new Timer( callback: _ => { try { tokenSource.Token.ThrowIfCancellationRequested(); var status = _distributedCache.GetString(key); if (!string.IsNullOrEmpty(status) && JsonConvert.DeserializeObject(status) == TaskStatus.Canceled) { tokenSource.Cancel(); return; } _distributedCache.SetString( key, _running, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expirationTime }); } catch (OperationCanceledException) { } }, state: null, dueTime: 0, period: (int)heartbeatInterval.TotalMilliseconds); await action(tokenSource.Token); } catch (OperationCanceledException) { } finally { await _distributedCache.RemoveAsync(key); } } /// /// Tries to cancel a background task by flagging it for cancellation on the next heartbeat. /// /// The task's key /// The time after which the task will be considered stopped due to system shutdown or other causes, /// even if not explicitly canceled. /// A task that represents requesting cancellation of the task. Note that the completion of this task does not /// necessarily imply that the task has been canceled, only that cancellation has been requested. public async Task CancelTaskAsync(string key, TimeSpan expirationTime) { var status = await _distributedCache.GetStringAsync(key); if (!string.IsNullOrEmpty(status) && JsonConvert.DeserializeObject(status) != TaskStatus.Canceled) await _distributedCache.SetStringAsync( key, JsonConvert.SerializeObject(TaskStatus.Canceled), new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expirationTime }); } /// /// Check if a background task is running. /// /// The task's key /// A task that resolves to true if the background task is running; otherwise false public async Task IsTaskRunningAsync(string key) { return !string.IsNullOrEmpty(await _distributedCache.GetStringAsync(key)); } #endregion }