Skip to content
This repository was archived by the owner on Sep 5, 2019. It is now read-only.

Commit

Permalink
Added tests and improved some internals (#26)
Browse files Browse the repository at this point in the history
* Used TaskCreationOptions.RunContinuationsAsynchronously for most situations

* Improved code documentation for DataLoader

* Added package tags

* Changed package project url

* Wrote tests and put default values into one file
  • Loading branch information
rstaib authored Oct 4, 2018
1 parent 3eeeb32 commit 746a0be
Show file tree
Hide file tree
Showing 14 changed files with 193 additions and 22 deletions.
13 changes: 9 additions & 4 deletions src/Benchmark.Tests/TaskCompletionBufferBenchmarks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,19 @@ public void IterationSetup()
[Benchmark]
public bool TryAddSingle()
{
return _buffer.TryAdd(_keys[0], new TaskCompletionSource<int>());
return _buffer.TryAdd(_keys[0], new TaskCompletionSource<int>(
TaskCreationOptions.RunContinuationsAsynchronously));
}

[Benchmark]
public bool TryAddSingleTwice()
{
var result = false;

result = _buffer.TryAdd(_keys[0], new TaskCompletionSource<int>());
result = _buffer.TryAdd(_keys[0], new TaskCompletionSource<int>());
result = _buffer.TryAdd(_keys[0], new TaskCompletionSource<int>(
TaskCreationOptions.RunContinuationsAsynchronously));
result = _buffer.TryAdd(_keys[0], new TaskCompletionSource<int>(
TaskCreationOptions.RunContinuationsAsynchronously));

return result;
}
Expand All @@ -52,7 +55,9 @@ public bool TryAdd1000()

for (var i = 0; i < _keys.Length; i++)
{
result = _buffer.TryAdd(_keys[i], new TaskCompletionSource<int>());
result = _buffer.TryAdd(_keys[i],
new TaskCompletionSource<int>(
TaskCreationOptions.RunContinuationsAsynchronously));
}

return result;
Expand Down
59 changes: 59 additions & 0 deletions src/Core.Tests/DataLoaderBaseTests.cs
Original file line number Diff line number Diff line change
@@ -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<string, string> fetch = async keys =>
await Task.FromResult(new Result<string>[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<string, string> cache = null;

// act
Action verify = () => new CacheConstructor(cache);

// assert
Assert.Throws<ArgumentNullException>("cache", verify);
}

[Fact(DisplayName = "Constructor: Should not throw any exception")]
public void ConstructorBNoException()
{
// arrange
var cache = new TaskCache<string, string>(10, TimeSpan.Zero);

// act
Action verify = () => new CacheConstructor(cache);

// assert
Assert.Null(Record.Exception(verify));
}

#endregion
}
}
51 changes: 50 additions & 1 deletion src/Core.Tests/DataLoaderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string> fetch = async keys =>
await Task.FromResult(new Result<string>[0])
.ConfigureAwait(false);
var options = new DataLoaderOptions<string>
{
BatchRequestDelay = TimeSpan.FromMinutes(10)
};
var loader = new DataLoader<string, string>(options, fetch);

await Task.Delay(10);

Task<IReadOnlyList<string>> loadResult = loader.LoadAsync("Foo", "Bar");

await loader.DispatchAsync().ConfigureAwait(false);

// act
Func<Task> verify = () => loadResult;

// assert
await Assert.ThrowsAsync<InvalidOperationException>(verify)
.ConfigureAwait(false);
}

#endregion

#region Dispose
Expand All @@ -256,6 +283,27 @@ await Task.FromResult(new Result<string>[0])
Assert.Null(Record.Exception(verify));
}

[Fact(DisplayName = "Dispose: Should dispose and not throw any exception (batching & cache disablked)")]
public void DisposeNoExceptionNobatchingAndCaching()
{
// arrange
FetchDataDelegate<string, string> fetch = async keys =>
await Task.FromResult(new Result<string>[0])
.ConfigureAwait(false);
var options = new DataLoaderOptions<string>
{
Batching = false,
Caching = false
};
var loader = new DataLoader<string, string>(options, fetch);

// act
Action verify = () => loader.Dispose();

// assert
Assert.Null(Record.Exception(verify));
}

#endregion

#region LoadAsync(string key)
Expand Down Expand Up @@ -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
Expand Down
20 changes: 20 additions & 0 deletions src/Core.Tests/FakeDataLoaders/CacheConstructor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace GreenDonut.FakeDataLoaders
{
internal class CacheConstructor
: DataLoaderBase<string, string>
{
internal CacheConstructor(TaskCache<string, string> cache)
: base(cache)
{ }

protected override Task<IReadOnlyList<Result<string>>> Fetch(
IReadOnlyList<string> keys)
{
throw new NotImplementedException();
}
}
}
20 changes: 20 additions & 0 deletions src/Core.Tests/FakeDataLoaders/EmptyConstructor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace GreenDonut.FakeDataLoaders
{
internal class EmptyConstructor
: DataLoaderBase<string, string>
{
internal EmptyConstructor()
: base()
{ }

protected override Task<IReadOnlyList<Result<string>>> Fetch(
IReadOnlyList<string> keys)
{
throw new NotImplementedException();
}
}
}
2 changes: 1 addition & 1 deletion src/Core.Tests/TaskCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>(cacheSize,
slidingExpiration);
var key = "Foo";
Expand Down
2 changes: 1 addition & 1 deletion src/Core/Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<AssemblyName>GreenDonut</AssemblyName>
<RootNamespace>GreenDonut</RootNamespace>
<PackageId>GreenDonut</PackageId>
<Description>Green Donut is a DataLoader implementation for .net core and classic</Description>
<Description>Green Donut is a DataLoader implementation for .net core and classic.</Description>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
Expand Down
4 changes: 3 additions & 1 deletion src/Core/DataLoader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ namespace GreenDonut
/// instance per web request. -- facebook
///
/// A default <c>DataLoader</c> 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.
/// </summary>
/// <typeparam name="TKey">A key type</typeparam>
/// <typeparam name="TValue">A value type</typeparam>
Expand Down
16 changes: 9 additions & 7 deletions src/Core/DataLoaderBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace GreenDonut
/// SQL table or document name in a MongoDB database, given a batch loading
/// function. -- facebook
///
/// Each <c>DataLoader</c> instance contains a unique memoized cache. Use
/// Each <c>DataLoader</c> 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
Expand Down Expand Up @@ -63,8 +63,9 @@ protected DataLoaderBase(ITaskCache<TKey, TValue> cache)
/// </param>
protected DataLoaderBase(DataLoaderOptions<TKey> options)
: this(options, new TaskCache<TKey, TValue>(
options?.CacheSize ?? 1000,
options?.SlidingExpiration ?? TimeSpan.Zero))
options?.CacheSize ?? Defaults.CacheSize,
options?.SlidingExpiration ??
Defaults.SlidingExpiration))
{ }

/// <summary>
Expand Down Expand Up @@ -146,7 +147,8 @@ public Task<TValue> LoadAsync(TKey key)
return cachedValue;
}

var promise = new TaskCompletionSource<TValue>();
var promise = new TaskCompletionSource<TValue>(
TaskCreationOptions.RunContinuationsAsynchronously);

if (_options.Batching)
{
Expand All @@ -163,7 +165,7 @@ public Task<TValue> 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)
Expand Down Expand Up @@ -269,7 +271,7 @@ private async Task DispatchAsync(
else
{
promise.SetException(
Errors.CreateKeysAndValusMustMatch(1, results.Count));
Errors.CreateKeysAndValuesMustMatch(1, results.Count));
}
}

Expand Down Expand Up @@ -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++)
Expand Down
6 changes: 3 additions & 3 deletions src/Core/DataLoaderOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

/// <summary>
Expand Down
13 changes: 13 additions & 0 deletions src/Core/Defaults.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
2 changes: 1 addition & 1 deletion src/Core/Errors.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ namespace GreenDonut
{
internal static class Errors
{
public static InvalidOperationException CreateKeysAndValusMustMatch(
public static InvalidOperationException CreateKeysAndValuesMustMatch(
int keysCount,
int valuesCount)
{
Expand Down
3 changes: 2 additions & 1 deletion src/Core/TaskCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ internal class TaskCache<TKey, TValue>

public TaskCache(int size, TimeSpan slidingExpiration)
{
Size = (size < 10) ? 10 : size;
Size = (Defaults.MinimumCacheSize > size)
? Defaults.MinimumCacheSize : size;
SlidingExpirartion = slidingExpiration;

StartExpiredEntryDetectionCycle();
Expand Down
4 changes: 2 additions & 2 deletions src/Package.props
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
<Company>ChilliCream</Company>
<Copyright>Copyright © 2018 ChilliCream (Michael &amp; Rafael Staib)</Copyright>
<PackageLicenseUrl>https://github.com/ChilliCream/greendonut/blob/master/LICENSE</PackageLicenseUrl>
<PackageProjectUrl>https://github.com/ChilliCream/greendonut</PackageProjectUrl>
<PackageProjectUrl>https://greendonut.io</PackageProjectUrl>
<RepositoryUrl>https://github.com/ChilliCream/greendonut</RepositoryUrl>
<PackageTags>DataLoader ChilliCream Facebook</PackageTags>
<PackageTags>DataLoader Batching Caching GraphQL ChilliCream Facebook</PackageTags>
<PackageReleaseNotes>Release notes: https://github.com/ChilliCream/greendonut/releases/$(PackageVersion)</PackageReleaseNotes>
<PackageRequireLicenseAcceptance>true</PackageRequireLicenseAcceptance>
<PackageIconUrl>https://cdn.rawgit.com/ChilliCream/greendonut-logo/master/img/greendonut-signet.png</PackageIconUrl>
Expand Down

0 comments on commit 746a0be

Please sign in to comment.