using Microsoft.Extensions.Caching.Memory; namespace Nop.Core.Caching; /// /// A distributed cache manager that locks the acquisition task /// public partial class MemoryCacheLocker : ILocker { #region Fields protected readonly IMemoryCache _memoryCache; #endregion #region Ctor public MemoryCacheLocker(IMemoryCache memoryCache) { _memoryCache = memoryCache; } #endregion #region Utilities /// /// Run action /// /// The key of the background task /// The time after which the lock will automatically be expired /// The action to perform /// A CancellationTokenSource for manually canceling the task /// protected virtual async Task RunAsync(string key, TimeSpan? expirationTime, Func action, CancellationTokenSource cancellationTokenSource = default) { var started = false; try { var tokenSource = _memoryCache.GetOrCreate(key, entry => new Lazy(() => { entry.AbsoluteExpirationRelativeToNow = expirationTime; entry.SetPriority(CacheItemPriority.NeverRemove); started = true; return cancellationTokenSource ?? new CancellationTokenSource(); }, true))?.Value; if (tokenSource != null && started) await action(tokenSource.Token); } catch (OperationCanceledException) { } finally { if (started) _memoryCache.Remove(key); } return started; } #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) { return await RunAsync(resource, expirationTime, _ => action()); } /// /// 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) { // We ignore expirationTime and heartbeatInterval here, as the cache is not shared with other instances, // and will be cleared on system failure anyway. The task is guaranteed to still be running as long as it is in the cache. await RunAsync(key, null, action, cancellationTokenSource); } /// /// 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 Task CancelTaskAsync(string key, TimeSpan expirationTime) { if (_memoryCache.TryGetValue(key, out Lazy tokenSource)) tokenSource.Value.Cancel(); return Task.CompletedTask; } /// /// 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 Task IsTaskRunningAsync(string key) { return Task.FromResult(_memoryCache.TryGetValue(key, out _)); } #endregion }