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}");
            }
        }