mirror of https://github.com/Squidex/squidex.git
Browse Source
# Conflicts: # backend/src/Migrations/Migrations/AddPatterns.cs # backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/MongoContentRepository_SnapshotStore.cs # backend/src/Squidex.Domain.Apps.Entities.MongoDb/Contents/Operations/QueryContentsByIds.cs # backend/src/Squidex.Domain.Apps.Entities/AppProvider.cs # backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsIndex.cs # backend/src/Squidex.Domain.Apps.Entities/Apps/Indexes/IAppsIndex.cs # backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs # backend/src/Squidex.Domain.Apps.Entities/IAppProvider.cs # backend/src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesIndex.cs # backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/ISchemasIndex.cs # backend/src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasIndex.cs # backend/src/Squidex.Web/CommandMiddlewares/EnrichWithSchemaIdCommandMiddleware.cs # backend/src/Squidex.Web/Pipeline/SchemaResolver.cs # backend/src/Squidex/Areas/Api/Controllers/Schemas/SchemasController.cs # backend/tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsIndexTests.cs # backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/DefaultWorkflowsValidatorTests.cs # backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs # backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs # backend/tests/Squidex.Domain.Apps.Entities.Tests/Rules/Guards/Triggers/ContentChangedTriggerTests.cs # backend/tests/Squidex.Web.Tests/CommandMiddlewares/EnrichWithSchemaIdCommandMiddlewareTests.cs # backend/tests/Squidex.Web.Tests/Pipeline/SchemaResolverTests.cspull/568/head
69 changed files with 1260 additions and 343 deletions
@ -0,0 +1,18 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Caching |
||||
|
{ |
||||
|
internal interface IPubSubSubscription |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,20 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Caching |
||||
|
{ |
||||
|
public sealed class ReplicatedCache : IReplicatedCache |
||||
|
{ |
||||
|
private readonly Guid instanceId = Guid.NewGuid(); |
||||
|
private readonly IMemoryCache memoryCache; |
||||
|
private readonly IPubSub pubSub; |
||||
|
|
||||
|
public class InvalidateMessage |
||||
|
{ |
||||
|
public Guid Source { get; set; } |
||||
|
|
||||
|
public string Key { get; set; } |
||||
|
} |
||||
|
|
||||
|
public ReplicatedCache(IMemoryCache memoryCache, IPubSub pubSub) |
||||
|
{ |
||||
|
Guard.NotNull(memoryCache, nameof(memoryCache)); |
||||
|
Guard.NotNull(pubSub, nameof(pubSub)); |
||||
|
|
||||
|
this.memoryCache = memoryCache; |
||||
|
|
||||
|
this.pubSub = pubSub; |
||||
|
this.pubSub.Subscribe(OnMessage); |
||||
|
} |
||||
|
|
||||
|
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) |
||||
|
{ |
||||
|
memoryCache.Set(key, value, expiration); |
||||
|
|
||||
|
if (invalidate) |
||||
|
{ |
||||
|
Invalidate(key); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Remove(string key) |
||||
|
{ |
||||
|
memoryCache.Remove(key); |
||||
|
|
||||
|
Invalidate(key); |
||||
|
} |
||||
|
|
||||
|
public bool TryGetValue(string key, out object? value) |
||||
|
{ |
||||
|
return memoryCache.TryGetValue(key, out value); |
||||
|
} |
||||
|
|
||||
|
private void Invalidate(string key) |
||||
|
{ |
||||
|
pubSub.Publish(new InvalidateMessage { Key = key, Source = instanceId }); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,32 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 sealed class SimplePubSub : IPubSub |
||||
|
{ |
||||
|
private readonly List<Action<object>> handlers = new List<Action<object>>(); |
||||
|
|
||||
|
public void Publish(object message) |
||||
|
{ |
||||
|
foreach (var handler in handlers) |
||||
|
{ |
||||
|
handler(message); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Subscribe(Action<object> handler) |
||||
|
{ |
||||
|
Guard.NotNull(handler, nameof(handler)); |
||||
|
|
||||
|
handlers.Add(handler); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,19 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,42 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,100 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.Caching.Memory; |
||||
|
using Microsoft.Extensions.Options; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Caching |
||||
|
{ |
||||
|
public class ReplicatedCacheTests |
||||
|
{ |
||||
|
private readonly IPubSub pubSub = new SimplePubSub(); |
||||
|
private readonly ReplicatedCache sut; |
||||
|
|
||||
|
public ReplicatedCacheTests() |
||||
|
{ |
||||
|
sut = new ReplicatedCache(CreateMemoryCache(), pubSub); |
||||
|
} |
||||
|
|
||||
|
[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 async Task Should_not_served_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); |
||||
|
var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub); |
||||
|
|
||||
|
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); |
||||
|
var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub); |
||||
|
|
||||
|
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); |
||||
|
var cache2 = new ReplicatedCache(CreateMemoryCache(), pubSub); |
||||
|
|
||||
|
cache1.Add("Key", 1, TimeSpan.FromMinutes(1), true); |
||||
|
cache2.Remove("Key"); |
||||
|
|
||||
|
AssertCache(cache1, "Key", null, false); |
||||
|
AssertCache(cache2, "Key", null, false); |
||||
|
} |
||||
|
|
||||
|
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())); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,79 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 |
||||
|
{ |
||||
|
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 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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,33 @@ |
|||||
|
import http from 'k6/http'; |
||||
|
import { check } from 'k6'; |
||||
|
import { variables, getBearerToken } from './shared.js'; |
||||
|
|
||||
|
export const options = { |
||||
|
stages: [ |
||||
|
{ duration: "2m", target: 500 }, |
||||
|
{ duration: "2m", target: 0 }, |
||||
|
], |
||||
|
thresholds: { |
||||
|
'http_req_duration': ['p(99)<300'], // 99% of requests must complete below 300ms
|
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export function setup() { |
||||
|
const token = getBearerToken('ci-semantic-search'); |
||||
|
|
||||
|
return { token }; |
||||
|
} |
||||
|
|
||||
|
export default function (data) { |
||||
|
const url = `${variables.serverUrl}/api/content/ci-semantic-search/test/5d648f76-7ae9-4141-a325-0c31ed155e5c`; |
||||
|
|
||||
|
const response = http.get(url, { |
||||
|
headers: { |
||||
|
Authorization: `Bearer ${data.token}` |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
check(response, { |
||||
|
'is status 200': (r) => r.status === 200, |
||||
|
}); |
||||
|
} |
||||
@ -0,0 +1,23 @@ |
|||||
|
import { check } from 'k6'; |
||||
|
import http from 'k6/http'; |
||||
|
|
||||
|
export const options = { |
||||
|
stages: [ |
||||
|
{ duration: "2m", target: 300 }, |
||||
|
{ duration: "2m", target: 300 }, |
||||
|
{ duration: "2m", target: 0 }, |
||||
|
], |
||||
|
thresholds: { |
||||
|
'http_req_duration': ['p(99)<300'], // 99% of requests must complete below 300ms
|
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export default function () { |
||||
|
const url = `https://test-api.k6.io/`; |
||||
|
|
||||
|
const response = http.get(url); |
||||
|
|
||||
|
check(response, { |
||||
|
'is status 200': (r) => r.status === 200, |
||||
|
}); |
||||
|
} |
||||
Loading…
Reference in new issue