public void ASecondThreadAccessingDuringTheFactoryStillGetsTheSameItem() { Guid DelayedFactory() { Thread.Sleep(TimeSpan.FromMilliseconds(300)); return(Guid.NewGuid()); } var cache = new OctopusCache(clock); Guid GetOrAdd() => cache !.GetOrAdd("key", DelayedFactory, TimeSpan.FromHours(1)); Guid?result1 = null; Guid?result2 = null; var thread1 = new Thread(() => result1 = GetOrAdd()); var thread2 = new Thread(() => result2 = GetOrAdd()); thread1.Start(); thread2.Start(); while (thread1.IsAlive || thread2.IsAlive) { Thread.Sleep(TimeSpan.FromMilliseconds(10)); } result1.Should().Be(result2); }
public void RetrievingByADifferentKeyShouldNotBlock() { var continueHandle = new EventWaitHandle(false, EventResetMode.ManualReset); Guid DelayedFactory() { continueHandle !.WaitOne(); return(Guid.NewGuid()); } var cache = new OctopusCache(clock); Guid?result2 = null; var thread1 = new Thread(() => cache.GetOrAdd("key1", DelayedFactory, TimeSpan.FromHours(1))); var thread2 = new Thread(() => result2 = cache.GetOrAdd("key2", factory, TimeSpan.FromHours(1))); thread1.Start(); thread2.Start(); while (thread2.IsAlive) { Thread.Sleep(TimeSpan.FromMilliseconds(10)); } result2.Should().HaveValue(); continueHandle.Set(); while (thread1.IsAlive) { Thread.Sleep(TimeSpan.FromMilliseconds(10)); } }
public void RemovingSomethingFromTheCacheWorksAsExpected() { var cacheKey = "test"; var initializationCalls = 0; var cache = new OctopusCache(clock); for (var i = 0; i < 10; i++) { var callCounter = i; var cached = cache.GetOrAdd(cacheKey, () => { initializationCalls++; return($"value-{callCounter}"); }, TimeSpan.FromHours(1)); if (callCounter <= 5) { cached.Should().Be("value-0", "the value I initially cached should be returned consistently until the item is expired or deleted manually"); } else { cached.Should().Be("value-6", "the value should be reevaluated after being removed from the cache"); } if (callCounter == 5) { cache.Delete(cacheKey); } } initializationCalls.Should().Be(2, "we should only initialize if there is no existing value in the cache and return the cached instance from there"); }
public void CachedItemRetrievedJustAfterExpiryReturnsADifferentItem() { var cache = new OctopusCache(clock); Guid GetOrAdd() => cache.GetOrAdd("key", factory, TimeSpan.FromHours(1)); var originalResult = GetOrAdd(); clock.WindForward(TimeSpan.FromHours(1).Add(TimeSpan.FromTicks(1))); GetOrAdd().Should().NotBe(originalResult); }
public void CachedItemRetrievedJustBeforeExpiryReturnsTheSameItem() { var cache = new OctopusCache(clock); Guid GetOrAdd() => cache.GetOrAdd("key", factory, TimeSpan.FromHours(1)); var originalResult = GetOrAdd(); clock.WindForward(TimeSpan.FromHours(1).Subtract(TimeSpan.FromTicks(1))); GetOrAdd().Should().Be(originalResult); }
public void CachedItemRetrievedInThePastReturnsSameItem() { var cache = new OctopusCache(clock); Guid GetOrAdd() => cache.GetOrAdd("key", factory, TimeSpan.FromHours(1)); var originalResult = GetOrAdd(); clock.WindForward(TimeSpan.FromSeconds(-2)); // Computer clock may have adjusted GetOrAdd().Should().Be(originalResult); }
public void UpdatingACacheEntryShouldReplaceIt() { var cache = new OctopusCache(clock); var cacheKey = "key-1"; var cached = cache.GetOrAdd(cacheKey, () => "value-1", TimeSpan.FromHours(1)); cached.Should().Be("value-1", "the value I initially cached should be returned until the cache is cleared"); cache.Update(cacheKey, "value-2", TimeSpan.FromHours(1)); cached = cache.GetOrAdd(cacheKey, () => "value-3", TimeSpan.FromHours(1)); cached.Should().Be("value-2", "the value the I updated it to should be returned until the cache expires again"); }
public void ClearingTheCacheShouldRemoveEverything() { var initializationCalls = 0; var cache = new OctopusCache(clock); for (var i = 0; i < 10; i++) { var callCounter = i; var cached = cache.GetOrAdd($"key-{callCounter}", () => { initializationCalls++; return($"value-{callCounter}"); }, TimeSpan.FromHours(1)); cached.Should().Be($"value-{callCounter}", "the value I initially cached should be returned consistently until the cache is cleared"); } cache.RemoveWhere(key => true); for (var i = 0; i < 10; i++) { var callCounter = i; var cached = cache.GetOrAdd($"key-{callCounter}", () => { initializationCalls++; return($"value-{callCounter}"); }, TimeSpan.FromHours(1)); cached.Should().Be($"value-{callCounter}", "the value I initially set after the first expired should be returned consistently until the cache expires again"); } initializationCalls.Should().Be(20, "after clearing the cache we should have to reinitialize everything"); }
public async Task WhenPassedATask_AndTheInitializerThrows_WeShouldEvictAndTryAgain() { var cacheKey = "test"; var initializationCalls = 0; var expectedExceptions = new List <DivideByZeroException>(); var cache = new OctopusCache(clock); for (var i = 0; i < 10; i++) { try { var cached = await cache.GetOrAdd(cacheKey, async() => await AsyncMethod(i), TimeSpan.FromSeconds(1)); cached.Should().Be("value-5", "the value I cached after failing the first few times should be returned consistently until the cache expires"); } catch (DivideByZeroException ex) { expectedExceptions.Add(ex); } } expectedExceptions.Should().HaveCount(5, "the first few initialization calls should have thrown an exception"); initializationCalls.Should().Be(6, "the initialization function should have failed a few times, then called one more time successfully once it starts working"); async Task <string> AsyncMethod(int callCounter) { await Task.Delay(1); initializationCalls++; // Fail on the first few calls, and succeed thereafter - simulates a SQL Server being unavailable for a while if (callCounter < 5) { throw new DivideByZeroException(); } return($"value-{callCounter}"); } }