mirror of https://github.com/Squidex/squidex.git
Browse Source
* Graphql memory cache. * Mini improvement. * Fix registrations. * Simplification. * Just some reformatting.pull/852/head
committed by
GitHub
52 changed files with 760 additions and 217 deletions
@ -0,0 +1,22 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.Options; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Caching; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Assets |
|||
{ |
|||
public sealed class AssetCache : QueryCache<DomainId, IEnrichedAssetEntity>, IAssetCache |
|||
{ |
|||
public AssetCache(IMemoryCache? memoryCache, IOptions<AssetOptions> options) |
|||
: base(options.Value.CanCache ? memoryCache : null) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Caching; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Assets |
|||
{ |
|||
public interface IAssetCache : IQueryCache<DomainId, IEnrichedAssetEntity> |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,22 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.Options; |
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Caching; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public sealed class ContentCache : QueryCache<DomainId, IEnrichedContentEntity>, IContentCache |
|||
{ |
|||
public ContentCache(IMemoryCache? memoryCache, IOptions<ContentOptions> options) |
|||
: base(options.Value.CanCache ? memoryCache : null) |
|||
{ |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,27 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using GraphQL.Types; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL.Types.Directives |
|||
{ |
|||
public sealed class CacheDirective : DirectiveGraphType |
|||
{ |
|||
public CacheDirective() |
|||
: base("cache", DirectiveLocation.Field, DirectiveLocation.FragmentSpread, DirectiveLocation.InlineFragment) |
|||
{ |
|||
Description = "Enable Memory Caching"; |
|||
|
|||
Arguments = new QueryArguments(new QueryArgument<IntGraphType> |
|||
{ |
|||
Name = "duration", |
|||
Description = "Cache duration in seconds.", |
|||
DefaultValue = 600 |
|||
}); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,16 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Squidex.Infrastructure; |
|||
using Squidex.Infrastructure.Caching; |
|||
|
|||
namespace Squidex.Domain.Apps.Entities.Contents |
|||
{ |
|||
public interface IContentCache : IQueryCache<DomainId, IEnrichedContentEntity> |
|||
{ |
|||
} |
|||
} |
|||
@ -0,0 +1,18 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Infrastructure.Caching |
|||
{ |
|||
public interface IQueryCache<TKey, T> where TKey : notnull where T : class, IWithId<TKey> |
|||
{ |
|||
void SetMany(IEnumerable<(TKey, T?)> results, |
|||
TimeSpan? permanentDuration = null); |
|||
|
|||
Task<List<T>> CacheOrQueryAsync(IEnumerable<TKey> keys, Func<IEnumerable<TKey>, Task<IEnumerable<T>>> query, |
|||
TimeSpan? permanentDuration = null); |
|||
} |
|||
} |
|||
@ -0,0 +1,94 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Collections.Concurrent; |
|||
using Microsoft.Extensions.Caching.Memory; |
|||
|
|||
namespace Squidex.Infrastructure.Caching |
|||
{ |
|||
public class QueryCache<TKey, T> : IQueryCache<TKey, T> where TKey : notnull where T : class, IWithId<TKey> |
|||
{ |
|||
private readonly ConcurrentDictionary<TKey, T?> entries = new ConcurrentDictionary<TKey, T?>(); |
|||
private readonly IMemoryCache? memoryCache; |
|||
|
|||
public QueryCache(IMemoryCache? memoryCache = null) |
|||
{ |
|||
this.memoryCache = memoryCache; |
|||
} |
|||
|
|||
public void SetMany(IEnumerable<(TKey, T?)> results, |
|||
TimeSpan? permanentDuration = null) |
|||
{ |
|||
Guard.NotNull(results); |
|||
|
|||
foreach (var (key, value) in results) |
|||
{ |
|||
Set(key, value, permanentDuration); |
|||
} |
|||
} |
|||
|
|||
private void Set(TKey key, T? value, |
|||
TimeSpan? permanentDuration = null) |
|||
{ |
|||
entries[key] = value; |
|||
|
|||
if (memoryCache != null && permanentDuration > TimeSpan.Zero) |
|||
{ |
|||
memoryCache.Set(key, value, permanentDuration.Value); |
|||
} |
|||
} |
|||
|
|||
public async Task<List<T>> CacheOrQueryAsync(IEnumerable<TKey> keys, Func<IEnumerable<TKey>, Task<IEnumerable<T>>> query, |
|||
TimeSpan? permanentDuration = null) |
|||
{ |
|||
Guard.NotNull(keys); |
|||
Guard.NotNull(query); |
|||
|
|||
var items = GetMany(keys, permanentDuration.HasValue); |
|||
|
|||
var pendingIds = new HashSet<TKey>(keys.Where(key => !items.ContainsKey(key))); |
|||
|
|||
if (pendingIds.Count > 0) |
|||
{ |
|||
var queried = (await query(pendingIds)).ToDictionary(x => x.Id); |
|||
|
|||
foreach (var id in pendingIds) |
|||
{ |
|||
queried.TryGetValue(id, out var item); |
|||
|
|||
items[id] = item; |
|||
|
|||
Set(id, item, permanentDuration); |
|||
} |
|||
} |
|||
|
|||
return items.Values.NotNull().ToList(); |
|||
} |
|||
|
|||
private Dictionary<TKey, T?> GetMany(IEnumerable<TKey> keys, |
|||
bool fromPermanentCache = false) |
|||
{ |
|||
var result = new Dictionary<TKey, T?>(); |
|||
|
|||
foreach (var key in keys) |
|||
{ |
|||
if (entries.TryGetValue(key, out var value)) |
|||
{ |
|||
result[key] = value; |
|||
} |
|||
else if (fromPermanentCache && memoryCache != null && memoryCache.TryGetValue(key, out value)) |
|||
{ |
|||
result[key] = value; |
|||
|
|||
entries[key] = value; |
|||
} |
|||
} |
|||
|
|||
return result; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Infrastructure |
|||
{ |
|||
public interface IWithId<T> |
|||
{ |
|||
T Id { get; } |
|||
} |
|||
} |
|||
@ -0,0 +1,180 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.Extensions.Caching.Memory; |
|||
using Microsoft.Extensions.Options; |
|||
using Xunit; |
|||
|
|||
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
|
|||
|
|||
namespace Squidex.Infrastructure.Caching |
|||
{ |
|||
public class QueryCacheTests |
|||
{ |
|||
private readonly IMemoryCache memoryCache = new MemoryCache(Options.Create(new MemoryCacheOptions())); |
|||
|
|||
private record CachedEntry(int Value) : IWithId<int> |
|||
{ |
|||
public int Id => Value; |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_query_from_cache() |
|||
{ |
|||
var sut = new QueryCache<int, CachedEntry>(); |
|||
|
|||
var (queried, result) = await ConfigureAsync(sut, 1, 2); |
|||
|
|||
Assert.Equal(new[] { 1, 2 }, queried); |
|||
Assert.Equal(new[] { 1, 2 }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_query_pending_from_cache() |
|||
{ |
|||
var sut = new QueryCache<int, CachedEntry>(); |
|||
|
|||
var (queried1, result1) = await ConfigureAsync(sut, 1, 2); |
|||
var (queried2, result2) = await ConfigureAsync(sut, 1, 2, 3, 4); |
|||
|
|||
Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); |
|||
Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); |
|||
|
|||
Assert.Equal(new[] { 1, 2 }, result1.ToArray()); |
|||
Assert.Equal(new[] { 1, 2, 3, 4 }, result2.ToArray()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_query_pending_from_cache_if_manually_added() |
|||
{ |
|||
var sut = new QueryCache<int, CachedEntry>(); |
|||
|
|||
sut.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }); |
|||
|
|||
var (queried, result) = await ConfigureAsync(sut, 1, 2, 3, 4); |
|||
|
|||
Assert.Equal(new[] { 3, 4 }, queried); |
|||
Assert.Equal(new[] { 2, 3, 4 }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_query_pending_from_memory_cache_if_manually_added() |
|||
{ |
|||
var sut1 = new QueryCache<int, CachedEntry>(memoryCache); |
|||
var sut2 = new QueryCache<int, CachedEntry>(memoryCache); |
|||
|
|||
var cacheDuration = TimeSpan.FromSeconds(10); |
|||
|
|||
sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }, cacheDuration); |
|||
|
|||
var (queried, result) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); |
|||
|
|||
Assert.Equal(new[] { 3, 4 }, queried); |
|||
Assert.Equal(new[] { 2, 3, 4 }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_query_pending_from_memory_cache_if_manually_added_but_not_added_permanently() |
|||
{ |
|||
var sut1 = new QueryCache<int, CachedEntry>(memoryCache); |
|||
var sut2 = new QueryCache<int, CachedEntry>(memoryCache); |
|||
|
|||
sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }); |
|||
|
|||
var (queried, result) = await ConfigureAsync(sut2, 1, 2, 3, 4); |
|||
|
|||
Assert.Equal(new[] { 1, 2, 3, 4 }, queried); |
|||
Assert.Equal(new[] { 1, 2, 3, 4 }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_query_pending_from_memory_cache_if_manually_added_but_not_queried_permanently() |
|||
{ |
|||
var sut1 = new QueryCache<int, CachedEntry>(memoryCache); |
|||
var sut2 = new QueryCache<int, CachedEntry>(memoryCache); |
|||
|
|||
var cacheDuration = TimeSpan.FromSeconds(10); |
|||
|
|||
sut1.SetMany(new[] { (1, null), (2, new CachedEntry(2)) }, cacheDuration); |
|||
|
|||
var (queried, result) = await ConfigureAsync(sut2, 1, 2, 3, 4); |
|||
|
|||
Assert.Equal(new[] { 1, 2, 3, 4 }, queried); |
|||
Assert.Equal(new[] { 1, 2, 3, 4 }, result); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_query_again_if_failed_before() |
|||
{ |
|||
var sut = new QueryCache<int, CachedEntry>(); |
|||
|
|||
var (queried1, result1) = await ConfigureAsync(sut, x => x > 1, default, 1, 2); |
|||
var (queried2, result2) = await ConfigureAsync(sut, 1, 2, 3, 4); |
|||
|
|||
Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); |
|||
Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); |
|||
|
|||
Assert.Equal(new[] { 2 }, result1.ToArray()); |
|||
Assert.Equal(new[] { 2, 3, 4 }, result2.ToArray()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_query_from_memory_cache() |
|||
{ |
|||
var sut1 = new QueryCache<int, CachedEntry>(memoryCache); |
|||
var sut2 = new QueryCache<int, CachedEntry>(memoryCache); |
|||
|
|||
var cacheDuration = TimeSpan.FromSeconds(10); |
|||
|
|||
var (queried1, result1) = await ConfigureAsync(sut1, x => true, cacheDuration, 1, 2); |
|||
var (queried2, result2) = await ConfigureAsync(sut2, x => true, cacheDuration, 1, 2, 3, 4); |
|||
|
|||
Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); |
|||
Assert.Equal(new[] { 3, 4 }, queried2.ToArray()); |
|||
|
|||
Assert.Equal(new[] { 1, 2 }, result1.ToArray()); |
|||
Assert.Equal(new[] { 1, 2, 3, 4 }, result2.ToArray()); |
|||
} |
|||
|
|||
[Fact] |
|||
public async Task Should_not_query_from_memory_cache_if_not_queried_permanently() |
|||
{ |
|||
var sut1 = new QueryCache<int, CachedEntry>(memoryCache); |
|||
var sut2 = new QueryCache<int, CachedEntry>(memoryCache); |
|||
|
|||
var (queried1, result1) = await ConfigureAsync(sut1, x => true, null, 1, 2); |
|||
var (queried2, result2) = await ConfigureAsync(sut2, x => true, null, 1, 2, 3, 4); |
|||
|
|||
Assert.Equal(new[] { 1, 2 }, queried1.ToArray()); |
|||
Assert.Equal(new[] { 1, 2, 3, 4 }, queried2.ToArray()); |
|||
|
|||
Assert.Equal(new[] { 1, 2 }, result1.ToArray()); |
|||
Assert.Equal(new[] { 1, 2, 3, 4 }, result2.ToArray()); |
|||
} |
|||
|
|||
private static Task<(int[], int[])> ConfigureAsync(IQueryCache<int, CachedEntry> sut, params int[] ids) |
|||
{ |
|||
return ConfigureAsync(sut, x => true, null, ids); |
|||
} |
|||
|
|||
private static async Task<(int[], int[])> ConfigureAsync(IQueryCache<int, CachedEntry> sut, Func<int, bool> predicate, TimeSpan? cacheDuration, params int[] ids) |
|||
{ |
|||
var queried = new HashSet<int>(); |
|||
|
|||
var result = await sut.CacheOrQueryAsync(ids, async pending => |
|||
{ |
|||
queried.AddRange(pending); |
|||
|
|||
await Task.Yield(); |
|||
|
|||
return pending.Where(predicate).Select(x => new CachedEntry(x)); |
|||
}, cacheDuration); |
|||
|
|||
return (queried.ToArray(), result.Select(x => x.Value).ToArray()); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue