Skip to content

Commit 8fbb518

Browse files
committed
Added promise cache interceptor. (#8190)
1 parent 5902695 commit 8fbb518

File tree

9 files changed

+273
-6
lines changed

9 files changed

+273
-6
lines changed

src/GreenDonut/src/GreenDonut.Abstractions/IPromiseCache.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Diagnostics.CodeAnalysis;
2+
13
namespace GreenDonut;
24

35
/// <summary>
@@ -92,7 +94,7 @@ public interface IPromiseCache
9294
void Publish<T>(T value);
9395

9496
/// <summary>
95-
/// Publishes the values to the cache subscribers without adding it to the cache iself.
97+
/// Publishes the values to the cache subscribers without adding it to the cache itself.
9698
/// This allows the subscribers to decide if they want to cache the values.
9799
/// </summary>
98100
/// <param name="values">
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
namespace GreenDonut;
2+
3+
/// <summary>
4+
/// Allows to implement a second-level cache for the DataLoader promise cache.
5+
/// </summary>
6+
public interface IPromiseCacheInterceptor
7+
{
8+
/// <summary>
9+
/// Gets a task from the cache if a task with the specified <paramref name="key"/> already
10+
/// exists; otherwise, the <paramref name="createPromise"/> factory is used to create a new
11+
/// task and add it to the cache.
12+
/// </summary>
13+
/// <param name="key">A cache entry key.</param>
14+
/// <param name="createPromise">A factory to create the new task.</param>
15+
/// <typeparam name="T">The task type.</typeparam>
16+
/// <returns>
17+
/// Returns either the retrieved or new task from the cache.
18+
/// </returns>
19+
/// <exception cref="ArgumentNullException">
20+
/// Throws if <paramref name="key"/> is <c>null</c>.
21+
/// </exception>
22+
/// <exception cref="ArgumentNullException">
23+
/// Throws if <paramref name="createPromise"/> is <c>null</c>.
24+
/// </exception>
25+
Promise<T> GetOrAddPromise<T>(PromiseCacheKey key, Func<PromiseCacheKey, Promise<T>> createPromise);
26+
27+
/// <summary>
28+
/// Tries to add a single task to the cache. It does nothing if the
29+
/// task exists already.
30+
/// </summary>
31+
/// <param name="key">A cache entry key.</param>
32+
/// <param name="promise">A task.</param>
33+
/// <typeparam name="T">The task type.</typeparam>
34+
/// <exception cref="ArgumentNullException">
35+
/// Throws if <paramref name="key"/> is <c>null</c>.
36+
/// </exception>
37+
/// <exception cref="ArgumentNullException">
38+
/// Throws if <paramref name="promise"/> is <c>null</c>.
39+
/// </exception>
40+
/// <returns>
41+
/// A value indicating whether the add was successful.
42+
/// </returns>
43+
bool TryAdd<T>(PromiseCacheKey key, Promise<T> promise);
44+
}

src/GreenDonut/src/GreenDonut/DependencyInjection/DataLoaderServiceCollectionExtensions.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
using System.Collections.Concurrent;
2-
using System.Collections.Frozen;
32
using GreenDonut;
43
using GreenDonut.DependencyInjection;
54
using Microsoft.Extensions.DependencyInjection.Extensions;
65
using Microsoft.Extensions.ObjectPool;
76

7+
// ReSharper disable once CheckNamespace
88
namespace Microsoft.Extensions.DependencyInjection;
99

1010
public static class DataLoaderServiceCollectionExtensions
@@ -66,7 +66,12 @@ public static IServiceCollection TryAddDataLoaderCore(
6666
services.TryAddScoped<IBatchScheduler, AutoBatchScheduler>();
6767

6868
services.TryAddSingleton(sp => PromiseCachePool.Create(sp.GetRequiredService<ObjectPoolProvider>()));
69-
services.TryAddScoped(sp => new PromiseCacheOwner(sp.GetRequiredService<ObjectPool<PromiseCache>>()));
69+
services.TryAddScoped(sp =>
70+
{
71+
var pool = sp.GetRequiredService<ObjectPool<PromiseCache>>();
72+
var interceptor = sp.GetService<IPromiseCacheInterceptor>();
73+
return new PromiseCacheOwner(pool, interceptor);
74+
});
7075

7176
services.TryAddSingleton<IDataLoaderDiagnosticEvents>(
7277
sp =>

src/GreenDonut/src/GreenDonut/PromiseCache.cs

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public PromiseCache(int size)
3838
/// <inheritdoc />
3939
public int Usage => _usage;
4040

41+
public IPromiseCacheInterceptor? Interceptor { get; set; }
42+
4143
/// <inheritdoc />
4244
public Task<T> GetOrAddTask<T>(PromiseCacheKey key, Func<PromiseCacheKey, Promise<T>> createPromise)
4345
{
@@ -51,12 +53,15 @@ public Task<T> GetOrAddTask<T>(PromiseCacheKey key, Func<PromiseCacheKey, Promis
5153
throw new ArgumentNullException(nameof(createPromise));
5254
}
5355

56+
var interceptor = Interceptor;
5457
var read = true;
5558

5659
var entry = _map.GetOrAdd(key, k =>
5760
{
5861
read = false;
59-
return AddNewEntry(k, createPromise(k));
62+
return interceptor is null
63+
? AddNewEntry(k, createPromise(k))
64+
: new Entry(k, interceptor.GetOrAddPromise(k, createPromise));
6065
});
6166

6267
if (read)
@@ -95,6 +100,11 @@ public bool TryAdd<T>(PromiseCacheKey key, Promise<T> promise)
95100
return AddNewEntry(k, promise);
96101
});
97102

103+
if(!read)
104+
{
105+
Interceptor?.TryAdd(key, promise);
106+
}
107+
98108
if (!promise.IsClone)
99109
{
100110
promise.OnComplete(NotifySubscribers, new CacheAndKey(this, key));
@@ -124,6 +134,11 @@ public bool TryAdd<T>(PromiseCacheKey key, Func<Promise<T>> createPromise)
124134
return AddNewEntry(k, createPromise());
125135
});
126136

137+
if(!read)
138+
{
139+
Interceptor?.TryAdd(key, (Promise<T>)entry.Value);
140+
}
141+
127142
var promise = (Promise<T>)entry.Value;
128143

129144
if (!promise.IsClone)

src/GreenDonut/src/GreenDonut/PromiseCacheOwner.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,21 @@ public sealed class PromiseCacheOwner : IDisposable
1515
/// <summary>
1616
/// Rents a new cache from <see cref="PromiseCachePool.Shared"/>.
1717
/// </summary>
18-
public PromiseCacheOwner()
18+
public PromiseCacheOwner(IPromiseCacheInterceptor? interceptor = null)
1919
{
2020
_pool = PromiseCachePool.Shared;
2121
_cache = PromiseCachePool.Shared.Get();
22+
_cache.Interceptor = interceptor;
2223
}
2324

2425
/// <summary>
2526
/// Rents a new cache from the given <paramref name="pool"/>.
2627
/// </summary>
27-
public PromiseCacheOwner(ObjectPool<PromiseCache> pool)
28+
public PromiseCacheOwner(ObjectPool<PromiseCache> pool, IPromiseCacheInterceptor? interceptor = null)
2829
{
2930
_pool = pool ?? throw new ArgumentNullException(nameof(pool));
3031
_cache = pool.Get();
32+
_cache.Interceptor = interceptor;
3133
}
3234

3335
/// <summary>

src/GreenDonut/src/GreenDonut/PromiseCachePooledObjectPolicy.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ internal sealed class PromiseCachePooledObjectPolicy(int size) : PooledObjectPol
88

99
public override bool Return(PromiseCache obj)
1010
{
11+
obj.Interceptor = null;
1112
obj.Clear();
1213
return true;
1314
}

src/GreenDonut/test/GreenDonut.Tests/PromiseCacheTests.cs

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Collections.Concurrent;
2+
13
namespace GreenDonut;
24

35
public class PromiseCacheTests
@@ -9,6 +11,7 @@ public void ConstructorNoException()
911
var cacheSize = 1;
1012

1113
// act
14+
// ReSharper disable once ObjectCreationAsStatement
1215
void Verify() => new PromiseCache(cacheSize);
1316

1417
// assert
@@ -240,4 +243,90 @@ public async Task GetOrAddTaskWhenNothingIsCached_IntegerKey()
240243
// assert
241244
Assert.Equal("Quox", await resolved);
242245
}
246+
247+
[Fact]
248+
public async Task GetOrAddTask_When_SecondLevelEntry_Exists()
249+
{
250+
// arrange
251+
var cacheSize = 10;
252+
var secondLevel = new SecondLevelCache();
253+
var cache = new PromiseCache(cacheSize) { Interceptor = secondLevel };
254+
var key = new PromiseCacheKey("abc", "abc");
255+
256+
// act
257+
var resolved = cache.GetOrAddTask(key, _ => new Promise<string>(Task.FromResult("Quox")));
258+
259+
// assert
260+
Assert.Equal("def", await resolved);
261+
}
262+
263+
[Fact]
264+
public async Task GetOrAddTask_When_SecondLevelEntry_Not_Exists()
265+
{
266+
// arrange
267+
var cacheSize = 10;
268+
var secondLevel = new SecondLevelCache();
269+
var cache = new PromiseCache(cacheSize) { Interceptor = secondLevel };
270+
var key = new PromiseCacheKey("abc", "123");
271+
272+
// act
273+
await cache.GetOrAddTask(key, _ => new Promise<string>(Task.FromResult("quox")));
274+
275+
// assert
276+
var secondLevelEntry = (Task<string>)secondLevel.Cache[key];
277+
Assert.Equal("quox", await secondLevelEntry);
278+
}
279+
280+
[Fact]
281+
public async Task TryAddTask_To_SecondLevelCache_1()
282+
{
283+
// arrange
284+
var cacheSize = 10;
285+
var secondLevel = new SecondLevelCache();
286+
var cache = new PromiseCache(cacheSize) { Interceptor = secondLevel };
287+
var key = new PromiseCacheKey("abc", "123");
288+
289+
// act
290+
cache.TryAdd(key, () => new Promise<string>(Task.FromResult("quox")));
291+
292+
// assert
293+
var secondLevelEntry = (Task<string>)secondLevel.Cache[key];
294+
Assert.Equal("quox", await secondLevelEntry);
295+
}
296+
297+
[Fact]
298+
public async Task TryAddTask_To_SecondLevelCache_2()
299+
{
300+
// arrange
301+
var cacheSize = 10;
302+
var secondLevel = new SecondLevelCache();
303+
var cache = new PromiseCache(cacheSize) { Interceptor = secondLevel };
304+
var key = new PromiseCacheKey("abc", "123");
305+
306+
// act
307+
cache.TryAdd(key, new Promise<string>(Task.FromResult("quox")));
308+
309+
// assert
310+
var secondLevelEntry = (Task<string>)secondLevel.Cache[key];
311+
Assert.Equal("quox", await secondLevelEntry);
312+
}
313+
314+
public class SecondLevelCache : IPromiseCacheInterceptor
315+
{
316+
private readonly ConcurrentDictionary<PromiseCacheKey, Task> _cache = new()
317+
{
318+
[new PromiseCacheKey("abc", "abc")] = Task.FromResult("def")
319+
};
320+
321+
public ConcurrentDictionary<PromiseCacheKey, Task> Cache => _cache;
322+
323+
public Promise<T> GetOrAddPromise<T>(PromiseCacheKey key, Func<PromiseCacheKey, Promise<T>> createPromise)
324+
{
325+
var entry = _cache.GetOrAdd(key, _ => createPromise(key).Task);
326+
return new Promise<T>((Task<T>)entry);
327+
}
328+
329+
public bool TryAdd<T>(PromiseCacheKey key, Promise<T> promise)
330+
=> _cache.TryAdd(key, promise.Task);
331+
}
243332
}

src/HotChocolate/Data/test/Data.PostgreSQL.Tests/HotChocolate.Data.PostgreSQL.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
</ItemGroup>
1717

1818
<ItemGroup>
19+
<PackageReference Include="Microsoft.Extensions.Caching.Memory" />
1920
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" />
2021
<PackageReference Include="Squadron.PostgreSql" />
2122
<PackageReference Include="System.IO.Pipelines" />

0 commit comments

Comments
 (0)