#region Dapplo 2016 - GNU Lesser General Public License // Dapplo - building blocks for .NET applications // Copyright (C) 2016 Dapplo // // For more information see: http://dapplo.net/ // Dapplo repositories are hosted on GitHub: https://github.com/dapplo // // This file is part of Dapplo.Utils // // Dapplo.Utils is free software: you can redistribute it and/or modify // it under the terms of the GNU Lesser General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Dapplo.Utils 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 Lesser General Public License for more details. // // You should have a copy of the GNU Lesser General Public License // along with Dapplo.Utils. If not, see . #endregion #region Usings using System; using System.Runtime.Caching; using System.Threading; using System.Threading.Tasks; using Dapplo.Log.Facade; #endregion 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(CancellationToken)); /// /// 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(CancellationToken)) { 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(CancellationToken)) { 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(CancellationToken)) { 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(CancellationToken)) { 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; } } var result = _cache.AddOrGetExisting(key, completionSource.Task, cacheItemPolicy) as Task; // Test if we got an existing object or our own if (result != null && !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); var disposable = cacheEntryRemovedArguments.CacheItem.Value as IDisposable; if (disposable != null) { _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); } } }