Browse Source

Caching extracted (#587)

* Caching extracted

* Tests cleanup
pull/588/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
af56812c07
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs
  2. 2
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs
  3. 2
      backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs
  4. 14
      backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs
  5. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs
  6. 9
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  7. 12
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs
  8. 2
      backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs
  9. 2
      backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs
  10. 14
      backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs
  11. 2
      backend/src/Squidex.Infrastructure.Amazon/Squidex.Infrastructure.Amazon.csproj
  12. 79
      backend/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs
  13. 28
      backend/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs
  14. 22
      backend/src/Squidex.Infrastructure/Caching/ILocalCache.cs
  15. 18
      backend/src/Squidex.Infrastructure/Caching/IPubSub.cs
  16. 13
      backend/src/Squidex.Infrastructure/Caching/IPubSubSubscription.cs
  17. 20
      backend/src/Squidex.Infrastructure/Caching/IReplicatedCache.cs
  18. 123
      backend/src/Squidex.Infrastructure/Caching/LRUCache.cs
  19. 19
      backend/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs
  20. 36
      backend/src/Squidex.Infrastructure/Caching/LocalCacheExtensions.cs
  21. 103
      backend/src/Squidex.Infrastructure/Caching/ReplicatedCache.cs
  22. 14
      backend/src/Squidex.Infrastructure/Caching/ReplicatedCacheOptions.cs
  23. 32
      backend/src/Squidex.Infrastructure/Caching/SimplePubSub.cs
  24. 2
      backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs
  25. 2
      backend/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs
  26. 19
      backend/src/Squidex.Infrastructure/Orleans/IPubSubGrain.cs
  27. 19
      backend/src/Squidex.Infrastructure/Orleans/IPubSubGrainObserver.cs
  28. 2
      backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs
  29. 79
      backend/src/Squidex.Infrastructure/Orleans/OrleansPubSub.cs
  30. 42
      backend/src/Squidex.Infrastructure/Orleans/OrleansPubSubGrain.cs
  31. 3
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  32. 11
      backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs
  33. 2
      backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs
  34. 14
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  35. 2
      backend/src/Squidex/Config/Orleans/OrleansServices.cs
  36. 3
      backend/src/Squidex/Squidex.csproj
  37. 5
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs
  38. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs
  39. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs
  40. 5
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs
  41. 123
      backend/tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs
  42. 98
      backend/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs
  43. 141
      backend/tests/Squidex.Infrastructure.Tests/Caching/ReplicatedCacheTests.cs
  44. 82
      backend/tests/Squidex.Infrastructure.Tests/Orleans/PubSubTests.cs

1
backend/src/Squidex.Domain.Apps.Core.Operations/Scripting/ContentWrapper/ContentDataObject.cs

@ -13,7 +13,6 @@ using Jint.Native;
using Jint.Native.Object;
using Jint.Runtime;
using Jint.Runtime.Descriptors;
using Orleans;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Infrastructure;

2
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cs

@ -6,8 +6,6 @@
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MongoDB.Driver;

2
backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs

@ -9,6 +9,7 @@ using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Squidex.Caching;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.Indexes;
using Squidex.Domain.Apps.Entities.Rules;
@ -16,7 +17,6 @@ using Squidex.Domain.Apps.Entities.Rules.Indexes;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Indexes;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Security;
namespace Squidex.Domain.Apps.Entities

14
backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs

@ -10,9 +10,9 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Orleans;
using Squidex.Caching;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
@ -36,7 +36,6 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
Guard.NotNull(replicatedCache, nameof(replicatedCache));
this.grainFactory = grainFactory;
this.replicatedCache = replicatedCache;
}
@ -149,7 +148,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
if (app != null)
{
CacheIt(app, false);
await CacheItAsync(app, false);
}
return app;
@ -227,7 +226,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
if (app != null)
{
CacheIt(app, true);
await CacheItAsync(app, true);
switch (context.Command)
{
@ -319,10 +318,11 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
return $"APPS_NAME_{name}";
}
private void CacheIt(IAppEntity app, bool publish)
private Task CacheItAsync(IAppEntity app, bool publish)
{
replicatedCache.Add(GetCacheKey(app.Id), app, CacheDuration, publish);
replicatedCache.Add(GetCacheKey(app.Name), app, CacheDuration, publish);
return Task.WhenAll(
replicatedCache.AddAsync(GetCacheKey(app.Id), app, CacheDuration, publish),
replicatedCache.AddAsync(GetCacheKey(app.Name), app, CacheDuration, publish));
}
}
}

2
backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOperationContext.cs

@ -16,11 +16,9 @@ using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.ValidateContent;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.Commands;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Translations;
using Squidex.Infrastructure.Validation;
#pragma warning disable IDE0016 // Use 'throw' expression

9
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs

@ -15,20 +15,21 @@ using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public sealed class CachingGraphQLService : CachingProviderBase, IGraphQLService
public sealed class CachingGraphQLService : IGraphQLService
{
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly IMemoryCache cache;
private readonly IServiceProvider resolver;
public CachingGraphQLService(IMemoryCache cache, IServiceProvider resolver)
: base(cache)
{
Guard.NotNull(cache, nameof(cache));
Guard.NotNull(resolver, nameof(resolver));
this.cache = cache;
this.resolver = resolver;
}
@ -83,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
var cacheKey = CreateCacheKey(app.Id, app.Version.ToString());
return Cache.GetOrCreateAsync(cacheKey, async entry =>
return cache.GetOrCreateAsync(cacheKey, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = CacheDuration;

12
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryParser.cs

@ -20,7 +20,6 @@ using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Log;
@ -33,20 +32,21 @@ using Squidex.Text;
namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
public class ContentQueryParser : CachingProviderBase
public class ContentQueryParser
{
private static readonly TimeSpan CacheTime = TimeSpan.FromMinutes(60);
private readonly IMemoryCache cache;
private readonly IJsonSerializer jsonSerializer;
private readonly ContentOptions options;
public ContentQueryParser(IMemoryCache cache, IJsonSerializer jsonSerializer, IOptions<ContentOptions> options)
: base(cache)
{
Guard.NotNull(jsonSerializer, nameof(jsonSerializer));
Guard.NotNull(cache, nameof(cache));
Guard.NotNull(options, nameof(options));
this.jsonSerializer = jsonSerializer;
this.cache = cache;
this.options = options.Value;
}
@ -138,7 +138,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
var cacheKey = BuildJsonCacheKey(context.App, schema, context.IsFrontendClient);
var result = Cache.GetOrCreate(cacheKey, entry =>
var result = cache.GetOrCreate(cacheKey, entry =>
{
entry.AbsoluteExpirationRelativeToNow = CacheTime;
@ -152,7 +152,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
var cacheKey = BuildEmdCacheKey(context.App, schema, context.IsFrontendClient);
var result = Cache.GetOrCreate<IEdmModel>(cacheKey, entry =>
var result = cache.GetOrCreate<IEdmModel>(cacheKey, entry =>
{
entry.AbsoluteExpirationRelativeToNow = CacheTime;

2
backend/src/Squidex.Domain.Apps.Entities/Contents/Text/State/CachingTextIndexerState.cs

@ -8,8 +8,8 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Squidex.Caching;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
namespace Squidex.Domain.Apps.Entities.Contents.Text.State
{

2
backend/src/Squidex.Domain.Apps.Entities/Rules/RuleEnqueuer.cs

@ -9,12 +9,12 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.EventSourcing;
namespace Squidex.Domain.Apps.Entities.Rules

14
backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs

@ -10,9 +10,9 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Orleans;
using Squidex.Caching;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Translations;
@ -33,7 +33,6 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
Guard.NotNull(replicatedCache, nameof(replicatedCache));
this.grainFactory = grainFactory;
this.replicatedCache = replicatedCache;
}
@ -99,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
if (schema != null)
{
CacheIt(schema, false);
await CacheItAsync(schema, false);
}
return schema;
@ -159,7 +158,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
if (schema != null)
{
CacheIt(schema, true);
await CacheItAsync(schema, true);
if (context.Command is DeleteSchema)
{
@ -221,10 +220,11 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
return $"SCHEMAS_ID_{appId}_{id}";
}
private void CacheIt(ISchemaEntity schema, bool publish)
private Task CacheItAsync(ISchemaEntity schema, bool publish)
{
replicatedCache.Add(GetCacheKey(schema.AppId.Id, schema.Id), schema, CacheDuration, publish);
replicatedCache.Add(GetCacheKey(schema.AppId.Id, schema.SchemaDef.Name), schema, CacheDuration, publish);
return Task.WhenAll(
replicatedCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.Id), schema, CacheDuration, publish),
replicatedCache.AddAsync(GetCacheKey(schema.AppId.Id, schema.SchemaDef.Name), schema, CacheDuration, publish));
}
}
}

2
backend/src/Squidex.Infrastructure.Amazon/Squidex.Infrastructure.Amazon.csproj

@ -6,7 +6,7 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.5.3.1" />
<PackageReference Include="AWSSDK.S3" Version="3.5.3.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
</ItemGroup>

79
backend/src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs

@ -1,79 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Concurrent;
using System.Threading;
using Squidex.Infrastructure.Tasks;
#pragma warning disable CS8601 // Possible null reference assignment.
namespace Squidex.Infrastructure.Caching
{
public sealed class AsyncLocalCache : ILocalCache
{
private static readonly AsyncLocal<ConcurrentDictionary<object, object>> LocalCache = new AsyncLocal<ConcurrentDictionary<object, object>>();
private static readonly AsyncLocalCleaner<ConcurrentDictionary<object, object>> Cleaner;
static AsyncLocalCache()
{
Cleaner = new AsyncLocalCleaner<ConcurrentDictionary<object, object>>(LocalCache);
}
public IDisposable StartContext()
{
LocalCache.Value = new ConcurrentDictionary<object, object>();
return Cleaner;
}
public void Add(object key, object? value)
{
var cacheKey = GetCacheKey(key);
var cache = LocalCache.Value;
if (cache != null)
{
cache[cacheKey] = value;
}
}
public void Remove(object key)
{
var cacheKey = GetCacheKey(key);
var cache = LocalCache.Value;
if (cache != null)
{
cache.TryRemove(cacheKey, out _);
}
}
public bool TryGetValue(object key, out object? value)
{
var cacheKey = GetCacheKey(key);
var cache = LocalCache.Value;
if (cache != null)
{
return cache.TryGetValue(cacheKey, out value);
}
value = null;
return false;
}
private static string GetCacheKey(object key)
{
return $"CACHE_{key}";
}
}
}

28
backend/src/Squidex.Infrastructure/Caching/CachingProviderBase.cs

@ -1,28 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.Extensions.Caching.Memory;
namespace Squidex.Infrastructure.Caching
{
public abstract class CachingProviderBase
{
private readonly IMemoryCache cache;
protected IMemoryCache Cache
{
get { return cache; }
}
protected CachingProviderBase(IMemoryCache cache)
{
Guard.NotNull(cache, nameof(cache));
this.cache = cache;
}
}
}

22
backend/src/Squidex.Infrastructure/Caching/ILocalCache.cs

@ -1,22 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.Caching
{
public interface ILocalCache
{
IDisposable StartContext();
void Add(object key, object? value);
void Remove(object key);
bool TryGetValue(object key, out object? value);
}
}

18
backend/src/Squidex.Infrastructure/Caching/IPubSub.cs

@ -1,18 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.Caching
{
public interface IPubSub
{
void Publish(object message);
void Subscribe(Action<object> handler);
}
}

13
backend/src/Squidex.Infrastructure/Caching/IPubSubSubscription.cs

@ -1,13 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Caching
{
internal interface IPubSubSubscription
{
}
}

20
backend/src/Squidex.Infrastructure/Caching/IReplicatedCache.cs

@ -1,20 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.Caching
{
public interface IReplicatedCache
{
void Add(string key, object? value, TimeSpan expiration, bool invalidate);
void Remove(string key);
bool TryGetValue(string key, out object? value);
}
}

123
backend/src/Squidex.Infrastructure/Caching/LRUCache.cs

@ -1,123 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
namespace Squidex.Infrastructure.Caching
{
public sealed class LRUCache<TKey, TValue> where TKey : notnull
{
private readonly Dictionary<TKey, LinkedListNode<LRUCacheItem<TKey, TValue>>> cacheMap = new Dictionary<TKey, LinkedListNode<LRUCacheItem<TKey, TValue>>>();
private readonly LinkedList<LRUCacheItem<TKey, TValue>> cacheHistory = new LinkedList<LRUCacheItem<TKey, TValue>>();
private readonly int capacity;
private readonly Action<TKey, TValue> itemEvicted;
public int Count
{
get { return cacheMap.Count; }
}
public IEnumerable<TKey> Keys
{
get { return cacheMap.Keys; }
}
public LRUCache(int capacity, Action<TKey, TValue>? itemEvicted = null)
{
Guard.GreaterThan(capacity, 0, nameof(capacity));
this.capacity = capacity;
this.itemEvicted = itemEvicted ?? ((key, value) => { });
}
public void Clear()
{
cacheHistory.Clear();
cacheMap.Clear();
}
public bool Set(TKey key, TValue value)
{
if (cacheMap.TryGetValue(key, out var node))
{
node.Value.Value = value;
cacheHistory.Remove(node);
cacheHistory.AddLast(node);
cacheMap[key] = node;
return true;
}
if (cacheMap.Count >= capacity)
{
RemoveFirst();
}
var cacheItem = new LRUCacheItem<TKey, TValue> { Key = key, Value = value };
node = new LinkedListNode<LRUCacheItem<TKey, TValue>>(cacheItem);
cacheMap.Add(key, node);
cacheHistory.AddLast(node);
return false;
}
public bool Remove(TKey key)
{
if (cacheMap.TryGetValue(key, out var node))
{
cacheMap.Remove(key);
cacheHistory.Remove(node);
return true;
}
return false;
}
public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value)
{
value = default!;
if (cacheMap.TryGetValue(key, out var node))
{
value = node.Value.Value;
cacheHistory.Remove(node);
cacheHistory.AddLast(node);
return true;
}
return false;
}
public bool Contains(TKey key)
{
return cacheMap.ContainsKey(key);
}
private void RemoveFirst()
{
var node = cacheHistory.First;
if (node != null)
{
itemEvicted(node.Value.Key, node.Value.Value);
cacheMap.Remove(node.Value.Key);
cacheHistory.RemoveFirst();
}
}
}
}

19
backend/src/Squidex.Infrastructure/Caching/LRUCacheItem.cs

@ -1,19 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
#pragma warning disable SA1401 // Fields must be private
#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable.
namespace Squidex.Infrastructure.Caching
{
internal class LRUCacheItem<TKey, TValue>
{
public TKey Key;
public TValue Value;
}
}

36
backend/src/Squidex.Infrastructure/Caching/LocalCacheExtensions.cs

@ -1,36 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Caching
{
public static class LocalCacheExtensions
{
public static async Task<T> GetOrCreateAsync<T>(this ILocalCache cache, object key, Func<Task<T>> task)
{
if (cache.TryGetValue(key, out var value))
{
if (value is T typed)
{
return typed;
}
else
{
return default!;
}
}
var result = await task();
cache.Add(key, result);
return result;
}
}
}

103
backend/src/Squidex.Infrastructure/Caching/ReplicatedCache.cs

@ -1,103 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
namespace Squidex.Infrastructure.Caching
{
public sealed class ReplicatedCache : IReplicatedCache
{
private readonly Guid instanceId = Guid.NewGuid();
private readonly IMemoryCache memoryCache;
private readonly IPubSub pubSub;
private readonly ReplicatedCacheOptions options;
public class InvalidateMessage
{
public Guid Source { get; set; }
public string Key { get; set; }
}
public ReplicatedCache(IMemoryCache memoryCache, IPubSub pubSub, IOptions<ReplicatedCacheOptions> options)
{
Guard.NotNull(memoryCache, nameof(memoryCache));
Guard.NotNull(pubSub, nameof(pubSub));
Guard.NotNull(options, nameof(options));
this.memoryCache = memoryCache;
this.pubSub = pubSub;
if (options.Value.Enable)
{
this.pubSub.Subscribe(OnMessage);
}
this.options = options.Value;
}
private void OnMessage(object message)
{
if (message is InvalidateMessage invalidate && invalidate.Source != instanceId)
{
memoryCache.Remove(invalidate.Key);
}
}
public void Add(string key, object? value, TimeSpan expiration, bool invalidate)
{
if (!options.Enable)
{
return;
}
memoryCache.Set(key, value, expiration);
if (invalidate)
{
Invalidate(key);
}
}
public void Remove(string key)
{
if (!options.Enable)
{
return;
}
memoryCache.Remove(key);
Invalidate(key);
}
public bool TryGetValue(string key, out object? value)
{
if (!options.Enable)
{
value = null;
return false;
}
return memoryCache.TryGetValue(key, out value);
}
private void Invalidate(string key)
{
if (!options.Enable)
{
return;
}
pubSub.Publish(new InvalidateMessage { Key = key, Source = instanceId });
}
}
}

14
backend/src/Squidex.Infrastructure/Caching/ReplicatedCacheOptions.cs

@ -1,14 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
namespace Squidex.Infrastructure.Caching
{
public sealed class ReplicatedCacheOptions
{
public bool Enable { get; set; }
}
}

32
backend/src/Squidex.Infrastructure/Caching/SimplePubSub.cs

@ -1,32 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
namespace Squidex.Infrastructure.Caching
{
public class SimplePubSub : IPubSub
{
private readonly List<Action<object>> handlers = new List<Action<object>>();
public virtual void Publish(object message)
{
foreach (var handler in handlers)
{
handler(message);
}
}
public virtual void Subscribe(Action<object> handler)
{
Guard.NotNull(handler, nameof(handler));
handlers.Add(handler);
}
}
}

2
backend/src/Squidex.Infrastructure/Commands/Rebuilder.cs

@ -10,7 +10,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Threading.Tasks.Dataflow;
using Squidex.Infrastructure.Caching;
using Squidex.Caching;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;

2
backend/src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs

@ -8,7 +8,7 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using Squidex.Infrastructure.Caching;
using Squidex.Caching;
namespace Squidex.Infrastructure.Orleans
{

19
backend/src/Squidex.Infrastructure/Orleans/IPubSubGrain.cs

@ -1,19 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Orleans;
namespace Squidex.Infrastructure.Orleans
{
public interface IPubSubGrain : IGrainWithStringKey
{
Task SubscribeAsync(IPubSubGrainObserver observer);
Task PublishAsync(object message);
}
}

19
backend/src/Squidex.Infrastructure/Orleans/IPubSubGrainObserver.cs

@ -1,19 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using Orleans;
namespace Squidex.Infrastructure.Orleans
{
public interface IPubSubGrainObserver : IGrainObserver
{
void Handle(object message);
void Subscribe(Action<object> handler);
}
}

2
backend/src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs

@ -8,7 +8,7 @@
using System;
using System.Threading.Tasks;
using Orleans;
using Squidex.Infrastructure.Caching;
using Squidex.Caching;
namespace Squidex.Infrastructure.Orleans
{

79
backend/src/Squidex.Infrastructure/Orleans/OrleansPubSub.cs

@ -1,79 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Orleans;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Orleans
{
public sealed class OrleansPubSub : IBackgroundProcess, IPubSub
{
private readonly IPubSubGrain pubSubGrain;
private readonly IPubSubGrainObserver pubSubGrainObserver = new Observer();
private readonly IGrainFactory grainFactory;
private sealed class Observer : IPubSubGrainObserver
{
private readonly List<Action<object>> subscriptions = new List<Action<object>>();
public void Handle(object message)
{
foreach (var subscription in subscriptions)
{
try
{
subscription(message);
}
catch
{
continue;
}
}
}
public void Subscribe(Action<object> handler)
{
subscriptions.Add(handler);
}
}
public OrleansPubSub(IGrainFactory grainFactory)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
this.grainFactory = grainFactory;
pubSubGrain = grainFactory.GetGrain<IPubSubGrain>(SingleGrain.Id);
}
public async Task StartAsync(CancellationToken ct)
{
var reference = await grainFactory.CreateObjectReference<IPubSubGrainObserver>(pubSubGrainObserver);
await pubSubGrain.SubscribeAsync(reference);
}
public void Publish(object message)
{
Guard.NotNull(message, nameof(message));
pubSubGrain.PublishAsync(message).Forget();
}
public void Subscribe(Action<object> handler)
{
Guard.NotNull(handler, nameof(handler));
pubSubGrainObserver.Subscribe(handler);
}
}
}

42
backend/src/Squidex.Infrastructure/Orleans/OrleansPubSubGrain.cs

@ -1,42 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using Orleans;
namespace Squidex.Infrastructure.Orleans
{
public sealed class OrleansPubSubGrain : Grain, IPubSubGrain
{
private readonly List<IPubSubGrainObserver> subscriptions = new List<IPubSubGrainObserver>();
public Task PublishAsync(object message)
{
foreach (var subscription in subscriptions)
{
try
{
subscription.Handle(message);
}
catch
{
continue;
}
}
return Task.CompletedTask;
}
public Task SubscribeAsync(IPubSubGrainObserver observer)
{
subscriptions.Add(observer);
return Task.CompletedTask;
}
}
}

3
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -15,7 +15,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Equals.Fody" Version="4.0.1" PrivateAssets="all" />
<PackageReference Include="FluentFTP" Version="32.4.5" />
<PackageReference Include="FluentFTP" Version="32.4.6" />
<PackageReference Include="Fody" Version="6.2.6">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@ -37,6 +37,7 @@
<PackageReference Include="NodaTime" Version="3.0.1" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="SixLabors.ImageSharp" Version="1.0.1" />
<PackageReference Include="Squidex.Caching" Version="1.1.0" />
<PackageReference Include="Squidex.Text" Version="1.1.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Collections.Immutable" Version="1.7.1" />

11
backend/src/Squidex.Infrastructure/UsageTracking/CachingUsageTracker.cs

@ -9,21 +9,22 @@ using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.Extensions.Caching.Memory;
using Squidex.Infrastructure.Caching;
namespace Squidex.Infrastructure.UsageTracking
{
public sealed class CachingUsageTracker : CachingProviderBase, IUsageTracker
public sealed class CachingUsageTracker : IUsageTracker
{
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(5);
private readonly IUsageTracker inner;
private readonly IMemoryCache cache;
public CachingUsageTracker(IUsageTracker inner, IMemoryCache cache)
: base(cache)
{
Guard.NotNull(inner, nameof(inner));
Guard.NotNull(cache, nameof(cache));
this.inner = inner;
this.cache = cache;
}
public Task<Dictionary<string, List<(DateTime, Counters)>>> QueryAsync(string key, DateTime fromDate, DateTime toDate)
@ -46,7 +47,7 @@ namespace Squidex.Infrastructure.UsageTracking
var cacheKey = string.Join("$", "Usage", nameof(GetForMonthAsync), key, date, category);
return Cache.GetOrCreateAsync(cacheKey, entry =>
return cache.GetOrCreateAsync(cacheKey, entry =>
{
entry.AbsoluteExpirationRelativeToNow = CacheDuration;
@ -60,7 +61,7 @@ namespace Squidex.Infrastructure.UsageTracking
var cacheKey = string.Join("$", "Usage", nameof(GetAsync), key, fromDate, toDate, category);
return Cache.GetOrCreateAsync(cacheKey, entry =>
return cache.GetOrCreateAsync(cacheKey, entry =>
{
entry.AbsoluteExpirationRelativeToNow = CacheDuration;

2
backend/src/Squidex.Web/Pipeline/LocalCacheMiddleware.cs

@ -7,8 +7,8 @@
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Squidex.Caching;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
namespace Squidex.Web.Pipeline
{

14
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -15,6 +15,7 @@ using Squidex.Areas.Api.Controllers.Contents.Generator;
using Squidex.Areas.Api.Controllers.News;
using Squidex.Areas.Api.Controllers.News.Service;
using Squidex.Areas.Api.Controllers.UI;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.Scripting;
using Squidex.Domain.Apps.Core.Scripting.Extensions;
using Squidex.Domain.Apps.Core.Tags;
@ -24,7 +25,6 @@ using Squidex.Domain.Apps.Entities.Contents.Counter;
using Squidex.Domain.Apps.Entities.Rules.UsageTracking;
using Squidex.Domain.Apps.Entities.Tags;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing.Grains;
using Squidex.Infrastructure.Orleans;
@ -49,6 +49,9 @@ namespace Squidex.Config.Domain
services.Configure<ReplicatedCacheOptions>(
config.GetSection("caching:replicated"));
services.AddReplicatedCache();
services.AddAsyncLocalCache();
services.AddSingletonAs(_ => SystemClock.Instance)
.As<IClock>();
@ -58,15 +61,6 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<GrainTagService>()
.As<ITagService>();
services.AddSingletonAs<AsyncLocalCache>()
.As<ILocalCache>();
services.AddSingletonAs<ReplicatedCache>()
.As<IReplicatedCache>();
services.AddSingletonAs<OrleansPubSub>()
.As<IPubSub>();
services.AddSingletonAs<JintScriptEngine>()
.AsOptional<IScriptEngine>();

2
backend/src/Squidex/Config/Orleans/OrleansServices.cs

@ -28,6 +28,8 @@ namespace Squidex.Config.Orleans
{
public static void ConfigureForSquidex(this ISiloBuilder builder, IConfiguration config)
{
builder.AddOrleansPubSub();
builder.ConfigureServices(siloServices =>
{
siloServices.AddSingleton<IMongoClientFactory, DefaultMongoClientFactory>();

3
backend/src/Squidex/Squidex.csproj

@ -58,7 +58,8 @@
<PackageReference Include="Orleans.Providers.MongoDB" Version="3.1.7" />
<PackageReference Include="OrleansDashboard" Version="3.1.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="4.7.0" PrivateAssets="all" />
<PackageReference Include="ReportGenerator" Version="4.7.1" PrivateAssets="all" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.1.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="5.5.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.Linq" Version="4.3.0" />

5
backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs

@ -10,12 +10,13 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Orleans;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.Apps;
using Squidex.Domain.Apps.Entities.Apps.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Security;
@ -45,7 +46,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
.Returns(indexByUser);
var cache =
new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(),
new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(A.Fake<ILogger<SimplePubSub>>()),
Options.Create(new ReplicatedCacheOptions { Enable = true }));
sut = new AppsIndex(grainFactory, cache);

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentChangedTriggerHandlerTests.cs

@ -11,6 +11,7 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.Contents;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules.EnrichedEvents;
@ -20,7 +21,6 @@ using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.EventSourcing;
using Xunit;

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/RuleEnqueuerTests.cs

@ -12,13 +12,13 @@ using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using NodaTime;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.HandleRules;
using Squidex.Domain.Apps.Core.Rules;
using Squidex.Domain.Apps.Core.Rules.Triggers;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.EventSourcing;
using Xunit;

5
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasIndexTests.cs

@ -10,12 +10,13 @@ using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Orleans;
using Squidex.Caching;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.Commands;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Validation;
@ -40,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
.Returns(index);
var cache =
new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(),
new ReplicatedCache(new MemoryCache(Options.Create(new MemoryCacheOptions())), new SimplePubSub(A.Fake<ILogger<SimplePubSub>>()),
Options.Create(new ReplicatedCacheOptions { Enable = true }));
sut = new SchemasIndex(grainFactory, cache);

123
backend/tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs

@ -1,123 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Xunit;
namespace Squidex.Infrastructure.Caching
{
public class AsyncLocalCacheTests
{
private readonly ILocalCache sut = new AsyncLocalCache();
private int called;
[Fact]
public async Task Should_add_item_to_cache_when_context_exists()
{
using (sut.StartContext())
{
sut.Add("Key", 1);
await Task.Delay(5);
AssertCache(sut, "Key", 1, true);
await Task.Delay(5);
sut.Remove("Key");
AssertCache(sut, "Key", null, false);
}
}
[Fact]
public async Task Should_not_add_item_to_cache_when_context_not_exists()
{
sut.Add("Key", 1);
await Task.Delay(5);
AssertCache(sut, "Key", null, false);
sut.Remove("Key");
await Task.Delay(5);
AssertCache(sut, "Key", null, false);
}
[Fact]
public async Task Should_call_creator_once_when_context_exists()
{
using (sut.StartContext())
{
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
await Task.Delay(5);
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(1, called);
Assert.Equal(1, value1);
Assert.Equal(1, value2);
}
}
[Fact]
public async Task Should_call_creator_twice_when_context_not_exists()
{
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
await Task.Delay(5);
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(2, called);
Assert.Equal(1, value1);
Assert.Equal(2, value2);
}
[Fact]
public async Task Should_call_async_creator_once_when_context_exists()
{
using (sut.StartContext())
{
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
await Task.Delay(5);
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(1, called);
Assert.Equal(1, value1);
Assert.Equal(1, value2);
}
}
[Fact]
public async Task Should_call_async_creator_twice_when_context_not_exists()
{
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
await Task.Delay(5);
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(2, called);
Assert.Equal(1, value1);
Assert.Equal(2, value2);
}
private static void AssertCache(ILocalCache cache, string key, object? expectedValue, bool expectedFound)
{
var found = cache.TryGetValue(key, out var value);
Assert.Equal(expectedFound, found);
Assert.Equal(expectedValue, value);
}
}
}

98
backend/tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs

@ -1,98 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using Xunit;
namespace Squidex.Infrastructure.Caching
{
public class LRUCacheTests
{
private readonly LRUCache<string, int> sut = new LRUCache<string, int>(10);
private readonly string key = "Key";
[Fact]
public void Should_always_override_when_setting_value()
{
sut.Set(key, 1);
sut.Set(key, 2);
Assert.True(sut.TryGetValue(key, out var value));
Assert.True(sut.Contains(key));
Assert.Equal(2, value);
}
[Fact]
public void Should_clear_items()
{
sut.Set("1", 1);
sut.Set("2", 2);
Assert.Equal(2, sut.Count);
sut.Clear();
Assert.Equal(0, sut.Count);
}
[Fact]
public void Should_remove_old_items_whentC_capacity_reached()
{
for (var i = 0; i < 15; i++)
{
sut.Set(i.ToString(), i);
}
for (var i = 0; i < 5; i++)
{
Assert.False(sut.TryGetValue(i.ToString(), out var value));
Assert.Equal(0, value);
}
for (var i = 5; i < 15; i++)
{
Assert.True(sut.TryGetValue(i.ToString(), out var value));
Assert.Equal(i, value);
}
}
[Fact]
public void Should_notify_about_evicted_items()
{
var evicted = new List<int>();
var cache = new LRUCache<int, int>(3, (key, _) => evicted.Add(key));
cache.Set(1, 1);
cache.Set(2, 2);
cache.Set(3, 3);
cache.Set(1, 1);
cache.Set(4, 4);
cache.Set(5, 5);
Assert.Equal(new List<int> { 2, 3 }, evicted);
}
[Fact]
public void Should_return_false_when_item_to_remove_does_not_exist()
{
Assert.False(sut.Remove(key));
}
[Fact]
public void Should_remove_inserted_item()
{
sut.Set(key, 2);
Assert.True(sut.Remove(key));
Assert.False(sut.Contains(key));
Assert.False(sut.TryGetValue(key, out var value));
Assert.Equal(0, value);
}
}
}

141
backend/tests/Squidex.Infrastructure.Tests/Caching/ReplicatedCacheTests.cs

@ -1,141 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Xunit;
namespace Squidex.Infrastructure.Caching
{
public class ReplicatedCacheTests
{
private readonly IPubSub pubSub = A.Fake<SimplePubSub>(options => options.CallsBaseMethods());
private readonly ReplicatedCacheOptions options = new ReplicatedCacheOptions { Enable = true };
private readonly ReplicatedCache sut;
public ReplicatedCacheTests()
{
sut = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
}
[Fact]
public void Should_serve_from_cache()
{
sut.Add("Key", 1, TimeSpan.FromMinutes(10), true);
AssertCache(sut, "Key", 1, true);
sut.Remove("Key");
AssertCache(sut, "Key", null, false);
}
[Fact]
public void Should_not_serve_from_cache_disabled()
{
options.Enable = false;
sut.Add("Key", 1, TimeSpan.FromMilliseconds(100), true);
AssertCache(sut, "Key", null, false);
}
[Fact]
public async Task Should_not_serve_from_cache_when_expired()
{
sut.Add("Key", 1, TimeSpan.FromMilliseconds(1), true);
await Task.Delay(100);
AssertCache(sut, "Key", null, false);
}
[Fact]
public void Should_not_invalidate_other_instances_when_item_added_and_flag_is_false()
{
var cache1 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
cache1.Add("Key", 1, TimeSpan.FromMinutes(1), false);
cache2.Add("Key", 2, TimeSpan.FromMinutes(1), false);
AssertCache(cache1, "Key", 1, true);
AssertCache(cache2, "Key", 2, true);
}
[Fact]
public void Should_invalidate_other_instances_when_item_added_and_flag_is_true()
{
var cache1 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
cache1.Add("Key", 1, TimeSpan.FromMinutes(1), true);
cache2.Add("Key", 2, TimeSpan.FromMinutes(1), true);
AssertCache(cache1, "Key", null, false);
AssertCache(cache2, "Key", 2, true);
}
[Fact]
public void Should_invalidate_other_instances_when_item_removed()
{
var cache1 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub, Options.Create(options));
cache1.Add("Key", 1, TimeSpan.FromMinutes(1), true);
cache2.Remove("Key");
AssertCache(cache1, "Key", null, false);
AssertCache(cache2, "Key", null, false);
}
[Fact]
public void Should_send_invalidation_message_when_added_and_flag_is_true()
{
sut.Add("Key", 1, TimeSpan.FromMinutes(1), true);
A.CallTo(() => pubSub.Publish(A<object>._))
.MustHaveHappened();
}
[Fact]
public void Should_not_send_invalidation_message_when_added_flag_is_false()
{
sut.Add("Key", 1, TimeSpan.FromMinutes(1), false);
A.CallTo(() => pubSub.Publish(A<object>._))
.MustNotHaveHappened();
}
[Fact]
public void Should_not_send_invalidation_message_when_added_but_disabled()
{
options.Enable = false;
sut.Add("Key", 1, TimeSpan.FromMinutes(1), true);
A.CallTo(() => pubSub.Publish(A<object>._))
.MustNotHaveHappened();
}
private static void AssertCache(IReplicatedCache cache, string key, object? expectedValue, bool expectedFound)
{
var found = cache.TryGetValue(key, out var value);
Assert.Equal(expectedFound, found);
Assert.Equal(expectedValue, value);
}
private static MemoryCache CreateMemoryCache()
{
return new MemoryCache(Options.Create(new MemoryCacheOptions()));
}
}
}

82
backend/tests/Squidex.Infrastructure.Tests/Orleans/PubSubTests.cs

@ -1,82 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Orleans;
using Orleans.TestingHost;
using Xunit;
namespace Squidex.Infrastructure.Orleans
{
[Trait("Category", "Dependencies")]
public class PubSubTests
{
[Fact]
public async Task Simple_pubsub_tests()
{
var cluster =
new TestClusterBuilder(3)
.Build();
await cluster.DeployAsync();
var sent = new HashSet<Guid>
{
Guid.NewGuid(),
Guid.NewGuid(),
Guid.NewGuid()
};
var received1 = await CreateSubscriber(cluster.Client, sent.Count);
var received2 = await CreateSubscriber(cluster.Client, sent.Count);
var pubSub = new OrleansPubSub(cluster.Client);
foreach (var message in sent)
{
pubSub.Publish(message);
}
await Task.WhenAny(
Task.WhenAll(
received1,
received2
),
Task.Delay(10000));
Assert.True(received1.Result.SetEquals(sent));
Assert.True(received2.Result.SetEquals(sent));
}
private static async Task<Task<HashSet<Guid>>> CreateSubscriber(IGrainFactory grainFactory, int expectedCount)
{
var pubSub = new OrleansPubSub(grainFactory);
await pubSub.StartAsync(default);
var received = new HashSet<Guid>();
var receivedCompleted = new TaskCompletionSource<HashSet<Guid>>();
pubSub.Subscribe(message =>
{
if (message is Guid guid)
{
received.Add(guid);
}
if (received.Count == expectedCount)
{
receivedCompleted.TrySetResult(received);
}
});
return receivedCompleted.Task;
}
}
}
Loading…
Cancel
Save