From dc448538007e63d3098b485d9f5b64ee90e5443b Mon Sep 17 00:00:00 2001 From: Niek Jannink Date: Tue, 22 May 2018 12:00:46 -0500 Subject: [PATCH] Add background cache update functionality. --- .../AssetsServiceWithCache.cs | 46 +++++++--- .../Cache/DictionaryCache.cs | 61 +++++++++++-- .../Cache/IDictionaryCache.cs | 21 +++++ .../IAssetsServiceWithCache.cs | 34 ++++++- .../Lykke.Service.Assets.Client.csproj | 6 +- .../ServiceCollectionExtensions.cs | 19 ++-- .../Cache/DictionaryCacheTest.cs | 88 +++++++++++++++++++ .../Lykke.Service.Assets.Tests.csproj | 1 + 8 files changed, 246 insertions(+), 30 deletions(-) create mode 100644 tests/Lykke.Service.Assets.Tests/Cache/DictionaryCacheTest.cs diff --git a/client/Lykke.Service.Assets.Client/AssetsServiceWithCache.cs b/client/Lykke.Service.Assets.Client/AssetsServiceWithCache.cs index 9f214de5..8d3554e6 100644 --- a/client/Lykke.Service.Assets.Client/AssetsServiceWithCache.cs +++ b/client/Lykke.Service.Assets.Client/AssetsServiceWithCache.cs @@ -1,26 +1,32 @@ -using System.Collections.Generic; +using Lykke.Service.Assets.Client.Cache; +using Lykke.Service.Assets.Client.Models; +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; using System.Threading; using System.Threading.Tasks; -using Lykke.Service.Assets.Client.Cache; -using Lykke.Service.Assets.Client.Models; +using Common.Log; namespace Lykke.Service.Assets.Client { + /// public class AssetsServiceWithCache : IAssetsServiceWithCache { - private readonly IAssetsService _assetsService; - private readonly IDictionaryCache _assetsCache; + private readonly IAssetsService _assetsService; + private readonly IDictionaryCache _assetsCache; private readonly IDictionaryCache _assetPairsCache; + private readonly ILog _log; - - public AssetsServiceWithCache(IAssetsService assetsService, IDictionaryCache assetsCache, IDictionaryCache assetPairsCache) + /// + public AssetsServiceWithCache(IAssetsService assetsService, IDictionaryCache assetsCache, IDictionaryCache assetPairsCache, ILog log) { - _assetsService = assetsService; - _assetsCache = assetsCache; + _assetsService = assetsService; + _assetsCache = assetsCache; _assetPairsCache = assetPairsCache; + _log = log; } - + /// public async Task> GetAllAssetPairsAsync(CancellationToken cancellationToken = new CancellationToken()) { await _assetPairsCache.EnsureCacheIsUpdatedAsync(() => GetUncachedAssetPairsAsync(cancellationToken)); @@ -28,13 +34,18 @@ public AssetsServiceWithCache(IAssetsService assetsService, IDictionaryCache> GetAllAssetsAsync(CancellationToken cancellationToken = new CancellationToken()) + async Task> IAssetsServiceWithCache.GetAllAssetsAsync(CancellationToken cancellationToken) + => await GetAllAssetsAsync(false, cancellationToken); + + /// + public async Task> GetAllAssetsAsync(bool includeNonTradable, CancellationToken cancellationToken = new CancellationToken()) { await _assetsCache.EnsureCacheIsUpdatedAsync(() => GetUncachedAssetsAsync(cancellationToken)); return _assetsCache.GetAll(); } + /// public async Task TryGetAssetAsync(string assetId, CancellationToken cancellationToken = new CancellationToken()) { await _assetsCache.EnsureCacheIsUpdatedAsync(() => GetUncachedAssetsAsync(cancellationToken)); @@ -42,6 +53,7 @@ public AssetsServiceWithCache(IAssetsService assetsService, IDictionaryCache public async Task TryGetAssetPairAsync(string assetPairId, CancellationToken cancellationToken = new CancellationToken()) { await _assetPairsCache.EnsureCacheIsUpdatedAsync(() => GetUncachedAssetPairsAsync(cancellationToken)); @@ -49,16 +61,28 @@ public AssetsServiceWithCache(IAssetsService assetsService, IDictionaryCache public async Task UpdateAssetPairsCacheAsync(CancellationToken cancellationToken = new CancellationToken()) { _assetPairsCache.Update(await GetUncachedAssetPairsAsync(cancellationToken)); } + /// public async Task UpdateAssetsCacheAsync(CancellationToken cancellationToken = new CancellationToken()) { _assetsCache.Update(await GetUncachedAssetsAsync(cancellationToken)); } + /// + public IDisposable StartAutoCacheUpdate() + { + return new CompositeDisposable + { + _assetPairsCache.StartAutoUpdate(nameof(AssetsServiceWithCache), _log, () => GetUncachedAssetPairsAsync(new CancellationToken())), + _assetsCache.StartAutoUpdate(nameof(AssetsServiceWithCache), _log, () => GetUncachedAssetsAsync(new CancellationToken())) + }; + } + private async Task> GetUncachedAssetsAsync(CancellationToken cancellationToken) { return await _assetsService.AssetGetAllAsync(false, cancellationToken); diff --git a/client/Lykke.Service.Assets.Client/Cache/DictionaryCache.cs b/client/Lykke.Service.Assets.Client/Cache/DictionaryCache.cs index 0ffbcce2..1e47c44b 100644 --- a/client/Lykke.Service.Assets.Client/Cache/DictionaryCache.cs +++ b/client/Lykke.Service.Assets.Client/Cache/DictionaryCache.cs @@ -1,38 +1,79 @@ -using System; +using Common; +using Common.Log; +using System; using System.Collections.Generic; using System.Linq; +using System.Reactive.Disposables; +using System.Threading; using System.Threading.Tasks; namespace Lykke.Service.Assets.Client.Cache { + /// public class DictionaryCache : IDictionaryCache where T : ICacheItem { private readonly IDateTimeProvider _dateTimeProvider; - private readonly TimeSpan _cacheExpirationPeriod; + private readonly TimeSpan _cacheExpirationPeriod; private Dictionary _items; - private DateTime _cacheExpirationMoment; - + private DateTime _cacheExpirationMoment; + private bool _inAutoUpdate; + /// public DictionaryCache(IDateTimeProvider dateTimeProvider, TimeSpan cacheExpirationPeriod) { - _items = new Dictionary(); + _items = new Dictionary(); _cacheExpirationMoment = DateTime.MinValue; - _dateTimeProvider = dateTimeProvider; + _dateTimeProvider = dateTimeProvider; _cacheExpirationPeriod = cacheExpirationPeriod; } + /// + public IDisposable StartAutoUpdate(string componentName, ILog log, Func>> getAllAsync) + { + if (_inAutoUpdate) + { + throw new InvalidOperationException("Dictionary is already in auto update mode."); + } + + _inAutoUpdate = true; + async Task UpdateCache(ITimerTrigger trigger, TimerTriggeredHandlerArgs args, CancellationToken token) + { + await Update(getAllAsync); + } + + var timer = new TimerTrigger(componentName, _cacheExpirationPeriod, log, UpdateCache); + timer.Start(); + + return Disposable.Create(() => + { + _inAutoUpdate = false; + timer.Dispose(); + }); + } + + /// public async Task EnsureCacheIsUpdatedAsync(Func>> getAllItemsAsync) { - if (_cacheExpirationMoment < _dateTimeProvider.UtcNow) + if (_inAutoUpdate) { - var items = await getAllItemsAsync(); + return; + } - Update(items); + if (_cacheExpirationMoment < _dateTimeProvider.UtcNow) + { + await Update(getAllItemsAsync); } } + private async Task Update(Func>> getAllItemsAsync) + { + var items = await getAllItemsAsync(); + Update(items); + } + + /// public void Update(IEnumerable items) { _items = items.ToDictionary(p => p.Id, p => p); @@ -40,6 +81,7 @@ public void Update(IEnumerable items) _cacheExpirationMoment = _dateTimeProvider.UtcNow + _cacheExpirationPeriod; } + /// public T TryGet(string id) { _items.TryGetValue(id, out var pair); @@ -47,6 +89,7 @@ public T TryGet(string id) return pair; } + /// public IReadOnlyCollection GetAll() { return _items.Values; diff --git a/client/Lykke.Service.Assets.Client/Cache/IDictionaryCache.cs b/client/Lykke.Service.Assets.Client/Cache/IDictionaryCache.cs index f81be666..a12d9a3a 100644 --- a/client/Lykke.Service.Assets.Client/Cache/IDictionaryCache.cs +++ b/client/Lykke.Service.Assets.Client/Cache/IDictionaryCache.cs @@ -1,18 +1,39 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Common.Log; namespace Lykke.Service.Assets.Client.Cache { + /// + /// Simple in-memory client side cache. + /// public interface IDictionaryCache where T : ICacheItem { + /// + /// Starts an automatic updater that keeps the cache updated on a background thread. + /// + IDisposable StartAutoUpdate(string componentName, ILog log, Func>> getAllAsync); + + /// + /// Update the cache when cache has expired. + /// Task EnsureCacheIsUpdatedAsync(Func>> getAllAsync); + /// + /// Update the cache with given data. + /// void Update(IEnumerable items); + /// + /// Try to get cached item with given id. + /// T TryGet(string id); + /// + /// Get all cached items. + /// IReadOnlyCollection GetAll(); } } diff --git a/client/Lykke.Service.Assets.Client/IAssetsServiceWithCache.cs b/client/Lykke.Service.Assets.Client/IAssetsServiceWithCache.cs index e19efbcc..02d7cff2 100644 --- a/client/Lykke.Service.Assets.Client/IAssetsServiceWithCache.cs +++ b/client/Lykke.Service.Assets.Client/IAssetsServiceWithCache.cs @@ -1,22 +1,44 @@ -using System.Collections.Generic; +using Lykke.Service.Assets.Client.Models; +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Lykke.Service.Assets.Client.Models; namespace Lykke.Service.Assets.Client { + /// + /// Client side in-memory cached version of the . + /// public interface IAssetsServiceWithCache { + /// + /// Get all asset-pairs. + /// Task> GetAllAssetPairsAsync(CancellationToken cancellationToken = new CancellationToken()); + /// + /// Get all assets + /// + [Obsolete("Use GetAllAssetsAsync(bool) instead")] Task> GetAllAssetsAsync(CancellationToken cancellationToken = new CancellationToken()); + /// + /// Get all assets + /// + Task> GetAllAssetsAsync(bool includeNonTradable, CancellationToken cancellationToken = new CancellationToken()); + + /// + /// Try to find an asset with given id. + /// Task TryGetAssetAsync(string assetId, CancellationToken cancellationToken = new CancellationToken()); + /// + /// Try to find an asset-pair with given id. + /// Task TryGetAssetPairAsync(string assetPairId, CancellationToken cancellationToken = new CancellationToken()); /// - /// Forcibly updates client-side asset pairs cache + /// Forcibly updates client-side asset-pairs cache /// Task UpdateAssetPairsCacheAsync(CancellationToken cancellationToken = new CancellationToken()); @@ -24,5 +46,11 @@ public interface IAssetsServiceWithCache /// Forcibly updates client-side assets cache /// Task UpdateAssetsCacheAsync(CancellationToken cancellationToken = new CancellationToken()); + + /// + /// Starts an automatic update process that will keep the caches updated the background. + /// + /// the update process, when disposed the auto update will stop + IDisposable StartAutoCacheUpdate(); } } diff --git a/client/Lykke.Service.Assets.Client/Lykke.Service.Assets.Client.csproj b/client/Lykke.Service.Assets.Client/Lykke.Service.Assets.Client.csproj index 95ac412a..63ff8fb8 100644 --- a/client/Lykke.Service.Assets.Client/Lykke.Service.Assets.Client.csproj +++ b/client/Lykke.Service.Assets.Client/Lykke.Service.Assets.Client.csproj @@ -25,9 +25,11 @@ + - - + + + \ No newline at end of file diff --git a/client/Lykke.Service.Assets.Client/ServiceCollectionExtensions.cs b/client/Lykke.Service.Assets.Client/ServiceCollectionExtensions.cs index d3621d69..b2b742f2 100644 --- a/client/Lykke.Service.Assets.Client/ServiceCollectionExtensions.cs +++ b/client/Lykke.Service.Assets.Client/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Lykke.Service.Assets.Client.Cache; +using Common.Log; +using Lykke.Service.Assets.Client.Cache; using Lykke.Service.Assets.Client.Models; using Microsoft.Extensions.DependencyInjection; @@ -6,19 +7,27 @@ namespace Lykke.Service.Assets.Client { public static class ServiceCollectionExtensions { - public static void RegisterAssetsClient(this IServiceCollection services, AssetServiceSettings settings) + public static void RegisterAssetsClient(this IServiceCollection services, AssetServiceSettings settings, + ILog log) { services .AddSingleton(x => new AssetsService(settings.BaseUri, settings.Handlers)); services - .AddTransient(); + .AddSingleton>(x => + new DictionaryCache(new DateTimeProvider(), settings.AssetsCacheExpirationPeriod)); services - .AddSingleton>(x => new DictionaryCache(new DateTimeProvider(), settings.AssetsCacheExpirationPeriod)); + .AddSingleton>(x => + new DictionaryCache(new DateTimeProvider(), settings.AssetPairsCacheExpirationPeriod)); services - .AddSingleton>(x => new DictionaryCache(new DateTimeProvider(), settings.AssetPairsCacheExpirationPeriod)); + .AddSingleton(x => new AssetsServiceWithCache( + x.GetService(), + x.GetService>(), + x.GetService>(), + log + )); } } } diff --git a/tests/Lykke.Service.Assets.Tests/Cache/DictionaryCacheTest.cs b/tests/Lykke.Service.Assets.Tests/Cache/DictionaryCacheTest.cs new file mode 100644 index 00000000..a55326f8 --- /dev/null +++ b/tests/Lykke.Service.Assets.Tests/Cache/DictionaryCacheTest.cs @@ -0,0 +1,88 @@ +using Lykke.Service.Assets.Client.Cache; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Common.Log; + +namespace Lykke.Service.Assets.Tests.Cache +{ + [TestClass] + public class DictionaryCacheTest + { + private int _getItemsCallCount; + + [TestInitialize] + public void Setup() + { + _getItemsCallCount = 0; + } + + [TestMethod] + public async Task TestExpiringCache() + { + var date = DateTime.UtcNow; + var provider = new Mock(); + provider.SetupGet(x => x.UtcNow).Returns(date); + + var sot = new DictionaryCache(provider.Object, TimeSpan.FromSeconds(30)); + + await sot.EnsureCacheIsUpdatedAsync(GetItems); + + Assert.IsTrue(sot.GetAll().Count > 0); + Assert.AreEqual(1, _getItemsCallCount); + + provider.SetupGet(x => x.UtcNow).Returns(date.AddMinutes(1)); + await sot.EnsureCacheIsUpdatedAsync(GetItems); + Assert.AreEqual(2, _getItemsCallCount); + + provider.SetupGet(x => x.UtcNow).Returns(date.AddSeconds(29)); + + await sot.EnsureCacheIsUpdatedAsync(GetItems); + Assert.AreEqual(2, _getItemsCallCount); + } + + [TestMethod] + public async Task TestRefreshingCache() + { + var date = DateTime.UtcNow; + var provider = new Mock(); + provider.SetupGet(x => x.UtcNow).Returns(date); + + var sot = new DictionaryCache(provider.Object, TimeSpan.FromMilliseconds(100)); + using (sot.StartAutoUpdate("TestRefreshingCache", Mock.Of(), GetItems)) + { + Assert.IsTrue(sot.GetAll().Count > 0, "Dictionary should be filled after update"); + Assert.AreEqual(1, _getItemsCallCount, "Initial update should be called at start"); + + await Task.Delay(150); + + Assert.AreEqual(2, _getItemsCallCount, "Update should have been called after timer tick"); + } + + await Task.Delay(150); + + Assert.AreEqual(2, _getItemsCallCount, "Auto update should have stopped after dispose"); + } + + private Task> GetItems() + { + _getItemsCallCount++; + + IEnumerable result = new List + { + new CacheItem {Id = "1"}, + new CacheItem {Id = "2"} + }; + + return Task.FromResult(result); + } + + private class CacheItem : ICacheItem + { + public string Id { get; set; } + } + } +} diff --git a/tests/Lykke.Service.Assets.Tests/Lykke.Service.Assets.Tests.csproj b/tests/Lykke.Service.Assets.Tests/Lykke.Service.Assets.Tests.csproj index 1438c861..d67232fd 100644 --- a/tests/Lykke.Service.Assets.Tests/Lykke.Service.Assets.Tests.csproj +++ b/tests/Lykke.Service.Assets.Tests/Lykke.Service.Assets.Tests.csproj @@ -12,6 +12,7 @@ +