diff --git a/src/NzbDrone.Common.Test/CacheTests/CachedFixture.cs b/src/NzbDrone.Common.Test/CacheTests/CachedFixture.cs index 9bb63c7e7..b21315b94 100644 --- a/src/NzbDrone.Common.Test/CacheTests/CachedFixture.cs +++ b/src/NzbDrone.Common.Test/CacheTests/CachedFixture.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading; using FluentAssertions; using NUnit.Framework; @@ -108,7 +108,7 @@ namespace NzbDrone.Common.Test.CacheTests public void should_clear_expired_when_they_expire() { int hitCount = 0; - _cachedString = new Cached(); + _cachedString = new Cached(rollingExpiry: true); for (int i = 0; i < 10; i++) { @@ -123,9 +123,9 @@ namespace NzbDrone.Common.Test.CacheTests Thread.Sleep(100); } - Thread.Sleep(1000); + Thread.Sleep(2000); - hitCount.Should().BeInRange(3, 7); + hitCount.Should().Be(1); _cachedString.Values.Should().HaveCount(0); } } diff --git a/src/NzbDrone.Common/Cache/CacheManager.cs b/src/NzbDrone.Common/Cache/CacheManager.cs index 6c080a02e..7d6bb128f 100644 --- a/src/NzbDrone.Common/Cache/CacheManager.cs +++ b/src/NzbDrone.Common/Cache/CacheManager.cs @@ -8,6 +8,7 @@ namespace NzbDrone.Common.Cache { ICached GetCache(Type host); ICached GetCache(Type host, string name); + ICached GetRollingCache(Type host, string name, TimeSpan defaultLifeTime); ICachedDictionary GetCacheDictionary(Type host, string name, Func> fetchFunc = null, TimeSpan? lifeTime = null); void Clear(); ICollection Caches { get; } @@ -43,6 +44,14 @@ namespace NzbDrone.Common.Cache return (ICached)_cache.Get(host.FullName + "_" + name, () => new Cached()); } + public ICached GetRollingCache(Type host, string name, TimeSpan defaultLifeTime) + { + Ensure.That(host, () => host).IsNotNull(); + Ensure.That(name, () => name).IsNotNullOrWhiteSpace(); + + return (ICached)_cache.Get(host.FullName + "_" + name, () => new Cached(defaultLifeTime, true)); + } + public ICachedDictionary GetCacheDictionary(Type host, string name, Func> fetchFunc = null, TimeSpan? lifeTime = null) { Ensure.That(host, () => host).IsNotNull(); diff --git a/src/NzbDrone.Common/Cache/Cached.cs b/src/NzbDrone.Common/Cache/Cached.cs index 8c953190e..8ab02f22b 100644 --- a/src/NzbDrone.Common/Cache/Cached.cs +++ b/src/NzbDrone.Common/Cache/Cached.cs @@ -1,8 +1,10 @@ -using System; +using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using NzbDrone.Common.EnsureThat; +using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Cache { @@ -29,40 +31,56 @@ namespace NzbDrone.Common.Cache } private readonly ConcurrentDictionary _store; + private readonly TimeSpan? _defaultLifeTime; + private readonly bool _rollingExpiry; - public Cached() + public Cached(TimeSpan? defaultLifeTime = null, bool rollingExpiry = false) { _store = new ConcurrentDictionary(); + _defaultLifeTime = defaultLifeTime; + _rollingExpiry = rollingExpiry; } - public void Set(string key, T value, TimeSpan? lifetime = null) + public void Set(string key, T value, TimeSpan? lifeTime = null) { Ensure.That(key, () => key).IsNotNullOrWhiteSpace(); - _store[key] = new CacheItem(value, lifetime); - if (lifetime != null) + _store[key] = new CacheItem(value, lifeTime ?? _defaultLifeTime); + + if (lifeTime != null) { - System.Threading.Tasks.Task.Delay(lifetime.Value).ContinueWith(t => _store.TryRemove(key, out var temp)); + ScheduleTryRemove(key, lifeTime.Value); } } public T Find(string key) { - CacheItem value; - _store.TryGetValue(key, out value); - - if (value == null) + CacheItem cacheItem; + if (!_store.TryGetValue(key, out cacheItem)) { return default(T); } - if (value.IsExpired()) + if (cacheItem.IsExpired()) { - _store.TryRemove(key, out value); - return default(T); + if (TryRemove(key, cacheItem)) + { + return default(T); + } + + if (!_store.TryGetValue(key, out cacheItem)) + { + return default(T); + } + } + + if (_rollingExpiry && _defaultLifeTime.HasValue) + { + _store.TryUpdate(key, new CacheItem(cacheItem.Object, _defaultLifeTime.Value), cacheItem); + ScheduleTryRemove(key, _defaultLifeTime.Value); } - return value.Object; + return cacheItem.Object; } public void Remove(string key) @@ -77,20 +95,32 @@ namespace NzbDrone.Common.Cache { Ensure.That(key, () => key).IsNotNullOrWhiteSpace(); + lifeTime = lifeTime ?? _defaultLifeTime; + CacheItem cacheItem; - T value; - if (!_store.TryGetValue(key, out cacheItem) || cacheItem.IsExpired()) + if (_store.TryGetValue(key, out cacheItem) && !cacheItem.IsExpired()) { - value = function(); - Set(key, value, lifeTime); + if (_rollingExpiry && lifeTime.HasValue) + { + _store.TryUpdate(key, new CacheItem(cacheItem.Object, lifeTime), cacheItem); + ScheduleTryRemove(key, lifeTime.Value); + } } else { - value = cacheItem.Object; + var newCacheItem = new CacheItem(function(), lifeTime); + if (cacheItem != null && _store.TryUpdate(key, newCacheItem, cacheItem)) + { + cacheItem = newCacheItem; + } + else + { + cacheItem = _store.GetOrAdd(key, newCacheItem); + } } - return value; + return cacheItem.Object; } public void Clear() @@ -100,9 +130,11 @@ namespace NzbDrone.Common.Cache public void ClearExpired() { - foreach (var cached in _store.Where(c => c.Value.IsExpired())) + var collection = (ICollection>)_store; + + foreach (var cached in _store.Where(c => c.Value.IsExpired()).ToList()) { - Remove(cached.Key); + collection.Remove(cached); } } @@ -113,5 +145,23 @@ namespace NzbDrone.Common.Cache return _store.Values.Select(c => c.Object).ToList(); } } + + private bool TryRemove(string key, CacheItem value) + { + var collection = (ICollection>)_store; + + return collection.Remove(new KeyValuePair(key, value)); + } + + private void ScheduleTryRemove(string key, TimeSpan lifeTime) + { + Task.Delay(lifeTime).ContinueWith(t => + { + if (_store.TryGetValue(key, out var cacheItem) && cacheItem.IsExpired()) + { + _store.TryRemove(key, out _); + } + }); + } } } diff --git a/src/NzbDrone.Core/Books/Services/AuthorService.cs b/src/NzbDrone.Core/Books/Services/AuthorService.cs index 9c9338076..bea58bdca 100644 --- a/src/NzbDrone.Core/Books/Services/AuthorService.cs +++ b/src/NzbDrone.Core/Books/Services/AuthorService.cs @@ -49,7 +49,7 @@ namespace NzbDrone.Core.Books _authorRepository = authorRepository; _eventAggregator = eventAggregator; _authorPathBuilder = authorPathBuilder; - _cache = cacheManager.GetCache>(GetType()); + _cache = cacheManager.GetRollingCache>(GetType(), "authorcache", TimeSpan.FromSeconds(30)); _logger = logger; }