diff --git a/src/Benchmark.Tests/TaskCompletionBufferBenchmarks.cs b/src/Benchmark.Tests/TaskCompletionBufferBenchmarks.cs index 5f605eb..8ec4c16 100644 --- a/src/Benchmark.Tests/TaskCompletionBufferBenchmarks.cs +++ b/src/Benchmark.Tests/TaskCompletionBufferBenchmarks.cs @@ -31,7 +31,8 @@ public void IterationSetup() [Benchmark] public bool TryAddSingle() { - return _buffer.TryAdd(_keys[0], new TaskCompletionSource()); + return _buffer.TryAdd(_keys[0], new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously)); } [Benchmark] @@ -39,8 +40,10 @@ public bool TryAddSingleTwice() { var result = false; - result = _buffer.TryAdd(_keys[0], new TaskCompletionSource()); - result = _buffer.TryAdd(_keys[0], new TaskCompletionSource()); + result = _buffer.TryAdd(_keys[0], new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously)); + result = _buffer.TryAdd(_keys[0], new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously)); return result; } @@ -52,7 +55,9 @@ public bool TryAdd1000() for (var i = 0; i < _keys.Length; i++) { - result = _buffer.TryAdd(_keys[i], new TaskCompletionSource()); + result = _buffer.TryAdd(_keys[i], + new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously)); } return result; diff --git a/src/Core.Tests/DataLoaderBaseTests.cs b/src/Core.Tests/DataLoaderBaseTests.cs new file mode 100644 index 0000000..27e023c --- /dev/null +++ b/src/Core.Tests/DataLoaderBaseTests.cs @@ -0,0 +1,59 @@ +using System; +using System.Threading.Tasks; +using GreenDonut.FakeDataLoaders; +using Xunit; + +namespace GreenDonut +{ + public class DataLoaderBaseTests + { + #region Constructor() + + [Fact(DisplayName = "Constructor: Should not throw any exception")] + public void ConstructorA() + { + // arrange + FetchDataDelegate fetch = async keys => + await Task.FromResult(new Result[0]) + .ConfigureAwait(false); + + // act + Action verify = () => new EmptyConstructor(); + + // assert + Assert.Null(Record.Exception(verify)); + } + + #endregion + + #region Constructor(cache) + + [Fact(DisplayName = "Constructor: Should throw an argument null exception for cache")] + public void ConstructorBCacheNull() + { + // arrange + TaskCache cache = null; + + // act + Action verify = () => new CacheConstructor(cache); + + // assert + Assert.Throws("cache", verify); + } + + [Fact(DisplayName = "Constructor: Should not throw any exception")] + public void ConstructorBNoException() + { + // arrange + var cache = new TaskCache(10, TimeSpan.Zero); + + // act + Action verify = () => new CacheConstructor(cache); + + // assert + Assert.Null(Record.Exception(verify)); + } + + #endregion + } +} diff --git a/src/Core.Tests/DataLoaderTests.cs b/src/Core.Tests/DataLoaderTests.cs index 39eed3f..eabdefd 100644 --- a/src/Core.Tests/DataLoaderTests.cs +++ b/src/Core.Tests/DataLoaderTests.cs @@ -235,6 +235,33 @@ await Task.FromResult(new[] { expectedResult }) await loadResult.ConfigureAwait(false)); } + [Fact(DisplayName = "DispatchAsync: Should return an invalid operation exception due to 2 missing values")] + public async Task DispatchAsyncKeysValuesNotMatching() + { + // arrange + FetchDataDelegate fetch = async keys => + await Task.FromResult(new Result[0]) + .ConfigureAwait(false); + var options = new DataLoaderOptions + { + BatchRequestDelay = TimeSpan.FromMinutes(10) + }; + var loader = new DataLoader(options, fetch); + + await Task.Delay(10); + + Task> loadResult = loader.LoadAsync("Foo", "Bar"); + + await loader.DispatchAsync().ConfigureAwait(false); + + // act + Func verify = () => loadResult; + + // assert + await Assert.ThrowsAsync(verify) + .ConfigureAwait(false); + } + #endregion #region Dispose @@ -256,6 +283,27 @@ await Task.FromResult(new Result[0]) Assert.Null(Record.Exception(verify)); } + [Fact(DisplayName = "Dispose: Should dispose and not throw any exception (batching & cache disablked)")] + public void DisposeNoExceptionNobatchingAndCaching() + { + // arrange + FetchDataDelegate fetch = async keys => + await Task.FromResult(new Result[0]) + .ConfigureAwait(false); + var options = new DataLoaderOptions + { + Batching = false, + Caching = false + }; + var loader = new DataLoader(options, fetch); + + // act + Action verify = () => loader.Dispose(); + + // assert + Assert.Null(Record.Exception(verify)); + } + #endregion #region LoadAsync(string key) @@ -438,7 +486,8 @@ public async Task LoadTest(int uniqueKeys, int maxRequests, return await dataLoader.LoadAsync(keyArray[index]) .ConfigureAwait(false); - }, TaskCreationOptions.DenyChildAttach).Unwrap(); + }, TaskCreationOptions.RunContinuationsAsynchronously) + .Unwrap(); } // assert diff --git a/src/Core.Tests/FakeDataLoaders/CacheConstructor.cs b/src/Core.Tests/FakeDataLoaders/CacheConstructor.cs new file mode 100644 index 0000000..ca7073e --- /dev/null +++ b/src/Core.Tests/FakeDataLoaders/CacheConstructor.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GreenDonut.FakeDataLoaders +{ + internal class CacheConstructor + : DataLoaderBase + { + internal CacheConstructor(TaskCache cache) + : base(cache) + { } + + protected override Task>> Fetch( + IReadOnlyList keys) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Core.Tests/FakeDataLoaders/EmptyConstructor.cs b/src/Core.Tests/FakeDataLoaders/EmptyConstructor.cs new file mode 100644 index 0000000..c7fab90 --- /dev/null +++ b/src/Core.Tests/FakeDataLoaders/EmptyConstructor.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace GreenDonut.FakeDataLoaders +{ + internal class EmptyConstructor + : DataLoaderBase + { + internal EmptyConstructor() + : base() + { } + + protected override Task>> Fetch( + IReadOnlyList keys) + { + throw new NotImplementedException(); + } + } +} diff --git a/src/Core.Tests/TaskCacheTests.cs b/src/Core.Tests/TaskCacheTests.cs index ee48a3e..d63d54e 100644 --- a/src/Core.Tests/TaskCacheTests.cs +++ b/src/Core.Tests/TaskCacheTests.cs @@ -398,7 +398,7 @@ public async Task VerifyExpirationFalse() { // arrange var cacheSize = 10; - var slidingExpiration = TimeSpan.FromMilliseconds(200); + var slidingExpiration = TimeSpan.FromMilliseconds(150); var cache = new TaskCache(cacheSize, slidingExpiration); var key = "Foo"; diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 3f3939f..510666b 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -5,7 +5,7 @@ GreenDonut GreenDonut GreenDonut - Green Donut is a DataLoader implementation for .net core and classic + Green Donut is a DataLoader implementation for .net core and classic. diff --git a/src/Core/DataLoader.cs b/src/Core/DataLoader.cs index ab4dbc1..6085883 100644 --- a/src/Core/DataLoader.cs +++ b/src/Core/DataLoader.cs @@ -16,7 +16,9 @@ namespace GreenDonut /// instance per web request. -- facebook /// /// A default DataLoader implementation which supports automatic and - /// manual batch dispatching. + /// manual batch dispatching. Also this implementation is using the default + /// cache implementation which useses the LRU (Least Recently Used) caching + /// algorithm for keeping track on which item has to be discarded first. /// /// A key type /// A value type diff --git a/src/Core/DataLoaderBase.cs b/src/Core/DataLoaderBase.cs index f3ae435..9f608e8 100644 --- a/src/Core/DataLoaderBase.cs +++ b/src/Core/DataLoaderBase.cs @@ -12,7 +12,7 @@ namespace GreenDonut /// SQL table or document name in a MongoDB database, given a batch loading /// function. -- facebook /// - /// Each DataLoader instance contains a unique memoized cache. Use + /// Each DataLoader instance contains a unique memorized cache. Use /// caution when used in long-lived applications or those which serve many /// users with different access permissions and consider creating a new /// instance per web request. -- facebook @@ -63,8 +63,9 @@ protected DataLoaderBase(ITaskCache cache) /// protected DataLoaderBase(DataLoaderOptions options) : this(options, new TaskCache( - options?.CacheSize ?? 1000, - options?.SlidingExpiration ?? TimeSpan.Zero)) + options?.CacheSize ?? Defaults.CacheSize, + options?.SlidingExpiration ?? + Defaults.SlidingExpiration)) { } /// @@ -146,7 +147,8 @@ public Task LoadAsync(TKey key) return cachedValue; } - var promise = new TaskCompletionSource(); + var promise = new TaskCompletionSource( + TaskCreationOptions.RunContinuationsAsynchronously); if (_options.Batching) { @@ -163,7 +165,7 @@ public Task LoadAsync(TKey key) // note: must run in the background; do not await here. Task.Factory.StartNew( () => DispatchAsync(resolvedKey, promise), - TaskCreationOptions.DenyChildAttach); + TaskCreationOptions.RunContinuationsAsynchronously); } if (_options.Caching) @@ -269,7 +271,7 @@ private async Task DispatchAsync( else { promise.SetException( - Errors.CreateKeysAndValusMustMatch(1, results.Count)); + Errors.CreateKeysAndValuesMustMatch(1, results.Count)); } } @@ -346,7 +348,7 @@ private void SetBatchResults( } else { - Exception error = Errors.CreateKeysAndValusMustMatch( + Exception error = Errors.CreateKeysAndValuesMustMatch( keys.Count, results.Count); for (var i = 0; i < keys.Count; i++) diff --git a/src/Core/DataLoaderOptions.cs b/src/Core/DataLoaderOptions.cs index 05b4aca..a497f66 100644 --- a/src/Core/DataLoaderOptions.cs +++ b/src/Core/DataLoaderOptions.cs @@ -17,10 +17,10 @@ public DataLoaderOptions() { AutoDispatching = true; Batching = true; - BatchRequestDelay = TimeSpan.FromMilliseconds(50); - CacheSize = 1000; + BatchRequestDelay = Defaults.BatchRequestDelay; + CacheSize = Defaults.CacheSize; Caching = true; - SlidingExpiration = TimeSpan.Zero; + SlidingExpiration = Defaults.SlidingExpiration; } /// diff --git a/src/Core/Defaults.cs b/src/Core/Defaults.cs new file mode 100644 index 0000000..31ea640 --- /dev/null +++ b/src/Core/Defaults.cs @@ -0,0 +1,13 @@ +using System; + +namespace GreenDonut +{ + internal static class Defaults + { + public static readonly int CacheSize = 1000; + public static readonly TimeSpan BatchRequestDelay = TimeSpan + .FromMilliseconds(50); + public static readonly int MinimumCacheSize = 10; + public static readonly TimeSpan SlidingExpiration = TimeSpan.Zero; + } +} diff --git a/src/Core/Errors.cs b/src/Core/Errors.cs index 3ae6717..6ecde6d 100644 --- a/src/Core/Errors.cs +++ b/src/Core/Errors.cs @@ -4,7 +4,7 @@ namespace GreenDonut { internal static class Errors { - public static InvalidOperationException CreateKeysAndValusMustMatch( + public static InvalidOperationException CreateKeysAndValuesMustMatch( int keysCount, int valuesCount) { diff --git a/src/Core/TaskCache.cs b/src/Core/TaskCache.cs index d2b1abd..c18fa5f 100644 --- a/src/Core/TaskCache.cs +++ b/src/Core/TaskCache.cs @@ -19,7 +19,8 @@ internal class TaskCache public TaskCache(int size, TimeSpan slidingExpiration) { - Size = (size < 10) ? 10 : size; + Size = (Defaults.MinimumCacheSize > size) + ? Defaults.MinimumCacheSize : size; SlidingExpirartion = slidingExpiration; StartExpiredEntryDetectionCycle(); diff --git a/src/Package.props b/src/Package.props index c5aacb3..4702a35 100644 --- a/src/Package.props +++ b/src/Package.props @@ -6,9 +6,9 @@ ChilliCream Copyright © 2018 ChilliCream (Michael & Rafael Staib) https://github.com/ChilliCream/greendonut/blob/master/LICENSE - https://github.com/ChilliCream/greendonut + https://greendonut.io https://github.com/ChilliCream/greendonut - DataLoader ChilliCream Facebook + DataLoader Batching Caching GraphQL ChilliCream Facebook Release notes: https://github.com/ChilliCream/greendonut/releases/$(PackageVersion) true https://cdn.rawgit.com/ChilliCream/greendonut-logo/master/img/greendonut-signet.png