/* * Greenshot - a free and open source screenshot tool * Copyright (C) 2007-2020 Thomas Braun, Jens Klingen, Robin Krom * * For more information see: http://getgreenshot.org/ * The Greenshot project is hosted on GitHub https://github.com/greenshot/greenshot * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 1 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ using System; using System.Runtime.Caching; using System.Threading; using System.Threading.Tasks; using Dapplo.Log; namespace GreenshotJiraPlugin { /// /// This abstract class builds a base for a simple async memory cache. /// /// Type for the key /// Type for the stored value public abstract class AsyncMemoryCache where TResult : class { private static readonly Task EmptyValueTask = Task.FromResult(null); private readonly SemaphoreSlim _semaphoreSlim = new SemaphoreSlim(1, 1); private readonly MemoryCache _cache = new MemoryCache(Guid.NewGuid().ToString()); private readonly LogSource _log = new LogSource(); /// /// Set the timespan for items to expire. /// public TimeSpan? ExpireTimeSpan { get; set; } /// /// Set the timespan for items to slide. /// public TimeSpan? SlidingTimeSpan { get; set; } /// /// Specifies if the RemovedCallback needs to be called /// If this is active, ActivateUpdateCallback should be false /// protected bool ActivateRemovedCallback { get; set; } = true; /// /// Specifies if the UpdateCallback needs to be called. /// If this is active, ActivateRemovedCallback should be false /// protected bool ActivateUpdateCallback { get; set; } = false; /// /// Implement this method, it should create an instance of TResult via the supplied TKey. /// /// TKey /// CancellationToken /// TResult protected abstract Task CreateAsync(TKey key, CancellationToken cancellationToken = default); /// /// Creates a key under which the object is stored or retrieved, default is a toString on the object. /// /// TKey /// string protected virtual string CreateKey(TKey keyObject) { return keyObject.ToString(); } /// /// Get an element from the cache, if this is not available call the create function. /// /// object for the key /// CancellationToken /// TResult public async Task DeleteAsync(TKey keyObject, CancellationToken cancellationToken = default) { var key = CreateKey(keyObject); await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); try { _cache.Remove(key); } finally { _semaphoreSlim.Release(); } } /// /// Get a task element from the cache, if this is not available return null. /// You probably want to call GetOrCreateAsync /// /// object for the key /// Task with TResult, null if no value public Task GetAsync(TKey keyObject) { var key = CreateKey(keyObject); return _cache.Get(key) as Task ?? EmptyValueTask; } /// /// Get a task element from the cache, if this is not available call the create function. /// /// object for the key /// CancellationToken /// Task with TResult public Task GetOrCreateAsync(TKey keyObject, CancellationToken cancellationToken = default) { var key = CreateKey(keyObject); return _cache.Get(key) as Task ?? GetOrCreateInternalAsync(keyObject, null, cancellationToken); } /// /// Get a task element from the cache, if this is not available call the create function. /// /// object for the key /// CacheItemPolicy for when you want more control over the item /// CancellationToken /// Task with TResult public Task GetOrCreateAsync(TKey keyObject, CacheItemPolicy cacheItemPolicy, CancellationToken cancellationToken = default) { var key = CreateKey(keyObject); return _cache.Get(key) as Task ?? GetOrCreateInternalAsync(keyObject, cacheItemPolicy, cancellationToken); } /// /// This takes care of the real async part of the code. /// /// /// CacheItemPolicy for when you want more control over the item /// CancellationToken /// TResult private async Task GetOrCreateInternalAsync(TKey keyObject, CacheItemPolicy cacheItemPolicy = null, CancellationToken cancellationToken = default) { var key = CreateKey(keyObject); var completionSource = new TaskCompletionSource(); if (cacheItemPolicy == null) { cacheItemPolicy = new CacheItemPolicy { AbsoluteExpiration = ExpireTimeSpan.HasValue ? DateTimeOffset.Now.Add(ExpireTimeSpan.Value) : ObjectCache.InfiniteAbsoluteExpiration, SlidingExpiration = SlidingTimeSpan ?? ObjectCache.NoSlidingExpiration }; if (ActivateUpdateCallback) { cacheItemPolicy.UpdateCallback = UpdateCallback; } if (ActivateRemovedCallback) { cacheItemPolicy.RemovedCallback = RemovedCallback; } } // Test if we got an existing object or our own if (_cache.AddOrGetExisting(key, completionSource.Task, cacheItemPolicy) is Task result && !completionSource.Task.Equals(result)) { return await result.ConfigureAwait(false); } await _semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false); try { result = _cache.AddOrGetExisting(key, completionSource.Task, cacheItemPolicy) as Task; if (result != null && !completionSource.Task.Equals(result)) { return await result.ConfigureAwait(false); } // Now, start the background task, which will set the completionSource with the correct response // ReSharper disable once MethodSupportsCancellation // ReSharper disable once UnusedVariable var ignoreBackgroundTask = Task.Run(async () => { try { var backgroundResult = await CreateAsync(keyObject, cancellationToken).ConfigureAwait(false); completionSource.TrySetResult(backgroundResult); } catch (TaskCanceledException) { completionSource.TrySetCanceled(); } catch (Exception ex) { completionSource.TrySetException(ex); } }); } finally { _semaphoreSlim.Release(); } return await completionSource.Task.ConfigureAwait(false); } /// /// Override to know when an item is removed, make sure to configure ActivateUpdateCallback / ActivateRemovedCallback /// /// CacheEntryRemovedArguments protected virtual void RemovedCallback(CacheEntryRemovedArguments cacheEntryRemovedArguments) { _log.Verbose().WriteLine("Item {0} removed due to {1}.", cacheEntryRemovedArguments.CacheItem.Key, cacheEntryRemovedArguments.RemovedReason); if (cacheEntryRemovedArguments.CacheItem.Value is IDisposable disposable) { _log.Debug().WriteLine("Disposed cached item."); disposable.Dispose(); } } /// /// Override to modify the cache behaviour when an item is about to be removed, make sure to configure /// ActivateUpdateCallback / ActivateRemovedCallback /// /// CacheEntryUpdateArguments protected virtual void UpdateCallback(CacheEntryUpdateArguments cacheEntryUpdateArguments) { _log.Verbose().WriteLine("Update request for {0} due to {1}.", cacheEntryUpdateArguments.Key, cacheEntryUpdateArguments.RemovedReason); } } }