mirror of https://github.com/Squidex/squidex.git
Browse Source
# Conflicts: # backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryReferrersAsync.cspull/590/head
66 changed files with 186 additions and 1191 deletions
@ -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}"; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
@ -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 |
|
||||
{ |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
@ -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(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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 }); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; } |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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())); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -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; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,3 +0,0 @@ |
|||||
.form-group { |
|
||||
margin-bottom: 1rem; |
|
||||
} |
|
||||
Loading…
Reference in new issue