mirror of https://github.com/Squidex/squidex.git
Browse Source
* Memory optimizations. * Interning of field keys. * Grain limiter service. * Tests and fixes for limiter. * Filter for limiter. * Tests for LRUCache. * Remove grain base classes. * Event enricher.pull/406/head
committed by
GitHub
72 changed files with 1497 additions and 836 deletions
@ -0,0 +1,65 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Newtonsoft.Json; |
||||
|
using Squidex.Infrastructure; |
||||
|
using Squidex.Infrastructure.Json.Newtonsoft; |
||||
|
using Squidex.Infrastructure.Json.Objects; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Contents.Json |
||||
|
{ |
||||
|
public sealed class ContentFieldDataConverter : JsonClassConverter<ContentFieldData> |
||||
|
{ |
||||
|
protected override void WriteValue(JsonWriter writer, ContentFieldData value, JsonSerializer serializer) |
||||
|
{ |
||||
|
writer.WriteStartObject(); |
||||
|
|
||||
|
foreach (var kvp in value) |
||||
|
{ |
||||
|
writer.WritePropertyName(kvp.Key); |
||||
|
|
||||
|
serializer.Serialize(writer, kvp.Value); |
||||
|
} |
||||
|
|
||||
|
writer.WriteEndObject(); |
||||
|
} |
||||
|
|
||||
|
protected override ContentFieldData ReadValue(JsonReader reader, Type objectType, JsonSerializer serializer) |
||||
|
{ |
||||
|
var result = new ContentFieldData(); |
||||
|
|
||||
|
while (reader.Read()) |
||||
|
{ |
||||
|
switch (reader.TokenType) |
||||
|
{ |
||||
|
case JsonToken.PropertyName: |
||||
|
var propertyName = reader.Value.ToString(); |
||||
|
|
||||
|
if (!reader.Read()) |
||||
|
{ |
||||
|
throw new JsonSerializationException("Unexpected end when reading Object."); |
||||
|
} |
||||
|
|
||||
|
var value = serializer.Deserialize<IJsonValue>(reader); |
||||
|
|
||||
|
if (Language.IsValidLanguage(propertyName) || propertyName == InvariantPartitioning.Key) |
||||
|
{ |
||||
|
propertyName = string.Intern(propertyName); |
||||
|
} |
||||
|
|
||||
|
result[propertyName] = value; |
||||
|
break; |
||||
|
case JsonToken.EndObject: |
||||
|
return result; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
throw new JsonSerializationException("Unexpected end when reading Object."); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,34 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System; |
|
||||
using Squidex.Domain.Apps.Events; |
|
||||
using Squidex.Infrastructure.Commands; |
|
||||
using Squidex.Infrastructure.EventSourcing; |
|
||||
using Squidex.Infrastructure.Log; |
|
||||
using Squidex.Infrastructure.States; |
|
||||
|
|
||||
namespace Squidex.Domain.Apps.Entities |
|
||||
{ |
|
||||
public abstract class SquidexDomainObjectGrainLogSnapshots<T> : LogSnapshotDomainObjectGrain<T> where T : IDomainState<T>, new() |
|
||||
{ |
|
||||
protected SquidexDomainObjectGrainLogSnapshots(IStore<Guid> store, ISemanticLog log) |
|
||||
: base(store, log) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public override void RaiseEvent(Envelope<IEvent> @event) |
|
||||
{ |
|
||||
if (@event.Payload is AppEvent appEvent) |
|
||||
{ |
|
||||
@event.SetAppId(appEvent.AppId.Id); |
|
||||
} |
|
||||
|
|
||||
base.RaiseEvent(@event); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,22 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.EventSourcing |
||||
|
{ |
||||
|
public class DefaultEventEnricher<TKey> : IEventEnricher<TKey> |
||||
|
{ |
||||
|
public virtual void Enrich(Envelope<IEvent> @event, TKey id) |
||||
|
{ |
||||
|
if (id is Guid guid) |
||||
|
{ |
||||
|
@event.SetAggregateId(guid); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.EventSourcing |
||||
|
{ |
||||
|
public interface IEventEnricher<T> |
||||
|
{ |
||||
|
void Enrich(Envelope<IEvent> @event, T id); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,77 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Orleans; |
||||
|
using Orleans.Runtime; |
||||
|
using Squidex.Infrastructure.Tasks; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Orleans |
||||
|
{ |
||||
|
public sealed class ActivationLimit : IActivationLimit, IDeactivater |
||||
|
{ |
||||
|
private readonly IGrainActivationContext context; |
||||
|
private readonly IActivationLimiter limiter; |
||||
|
private int maxActivations; |
||||
|
|
||||
|
public ActivationLimit(IGrainActivationContext context, IActivationLimiter limiter) |
||||
|
{ |
||||
|
Guard.NotNull(context, nameof(context)); |
||||
|
Guard.NotNull(limiter, nameof(limiter)); |
||||
|
|
||||
|
this.context = context; |
||||
|
this.limiter = limiter; |
||||
|
} |
||||
|
|
||||
|
public void ReportIAmAlive() |
||||
|
{ |
||||
|
if (maxActivations > 0) |
||||
|
{ |
||||
|
limiter.Register(context.GrainType, this, maxActivations); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void ReportIAmDead() |
||||
|
{ |
||||
|
if (maxActivations > 0) |
||||
|
{ |
||||
|
limiter.Unregister(context.GrainType, this); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void SetLimit(int maxActivations, TimeSpan lifetime) |
||||
|
{ |
||||
|
this.maxActivations = maxActivations; |
||||
|
|
||||
|
context.ObservableLifecycle?.Subscribe("Limiter", GrainLifecycleStage.Activate, |
||||
|
ct => |
||||
|
{ |
||||
|
var runtime = context.ActivationServices.GetRequiredService<IGrainRuntime>(); |
||||
|
|
||||
|
runtime.DelayDeactivation(context.GrainInstance, lifetime); |
||||
|
|
||||
|
ReportIAmAlive(); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
}, |
||||
|
ct => |
||||
|
{ |
||||
|
ReportIAmDead(); |
||||
|
|
||||
|
return TaskHelper.Done; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
void IDeactivater.Deactivate() |
||||
|
{ |
||||
|
var runtime = context.ActivationServices.GetRequiredService<IGrainRuntime>(); |
||||
|
|
||||
|
runtime.DeactivateOnIdle(context.GrainInstance); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,72 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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.Caching; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Orleans |
||||
|
{ |
||||
|
public sealed class ActivationLimiter : IActivationLimiter |
||||
|
{ |
||||
|
private readonly ConcurrentDictionary<Type, LastUsedInstances> instances = new ConcurrentDictionary<Type, LastUsedInstances>(); |
||||
|
|
||||
|
private sealed class LastUsedInstances |
||||
|
{ |
||||
|
private readonly LRUCache<IDeactivater, IDeactivater> recentUsedGrains; |
||||
|
private readonly ReaderWriterLockSlim lockSlim = new ReaderWriterLockSlim(); |
||||
|
|
||||
|
public LastUsedInstances(int limit) |
||||
|
{ |
||||
|
recentUsedGrains = new LRUCache<IDeactivater, IDeactivater>(limit, (key, _) => key.Deactivate()); |
||||
|
} |
||||
|
|
||||
|
public void Register(IDeactivater instance) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
lockSlim.EnterWriteLock(); |
||||
|
|
||||
|
recentUsedGrains.Set(instance, instance); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
lockSlim.ExitWriteLock(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Unregister(IDeactivater instance) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
lockSlim.EnterWriteLock(); |
||||
|
|
||||
|
recentUsedGrains.Remove(instance); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
lockSlim.ExitWriteLock(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void Register(Type grainType, IDeactivater deactivater, int maxActivations) |
||||
|
{ |
||||
|
var byType = instances.GetOrAdd(grainType, t => new LastUsedInstances(maxActivations)); |
||||
|
|
||||
|
byType.Register(deactivater); |
||||
|
} |
||||
|
|
||||
|
public void Unregister(Type grainType, IDeactivater deactivater) |
||||
|
{ |
||||
|
instances.TryGetValue(grainType, out var byType); |
||||
|
|
||||
|
byType?.Unregister(deactivater); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 sealed class ActivationLimiterFilter : IIncomingGrainCallFilter |
||||
|
{ |
||||
|
public Task Invoke(IIncomingGrainCallContext context) |
||||
|
{ |
||||
|
if (context.Grain is GrainBase grainBase) |
||||
|
{ |
||||
|
grainBase.ReportIAmAlive(); |
||||
|
} |
||||
|
|
||||
|
return context.Invoke(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,63 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Orleans; |
||||
|
using Orleans.Core; |
||||
|
using Orleans.Runtime; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Orleans |
||||
|
{ |
||||
|
public abstract class GrainBase : Grain |
||||
|
{ |
||||
|
protected GrainBase() |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
protected GrainBase(IGrainIdentity identity, IGrainRuntime runtime) |
||||
|
: base(identity, runtime) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
public void ReportIAmAlive() |
||||
|
{ |
||||
|
var limit = ServiceProvider.GetService<IActivationLimit>(); |
||||
|
|
||||
|
limit?.ReportIAmAlive(); |
||||
|
} |
||||
|
|
||||
|
public void ReportIAmDead() |
||||
|
{ |
||||
|
var limit = ServiceProvider.GetService<IActivationLimit>(); |
||||
|
|
||||
|
limit?.ReportIAmDead(); |
||||
|
} |
||||
|
|
||||
|
protected void TryDelayDeactivation(TimeSpan timeSpan) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
DelayDeactivation(timeSpan); |
||||
|
} |
||||
|
catch (InvalidOperationException) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected void TryDeactivateOnIdle() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
DeactivateOnIdle(); |
||||
|
} |
||||
|
catch (InvalidOperationException) |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,75 +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 Orleans; |
|
||||
using Squidex.Infrastructure.States; |
|
||||
using Squidex.Infrastructure.Tasks; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Orleans |
|
||||
{ |
|
||||
public abstract class GrainOfGuid<T> : Grain where T : class, new() |
|
||||
{ |
|
||||
private readonly IStore<Guid> store; |
|
||||
private IPersistence<T> persistence; |
|
||||
|
|
||||
protected T State { get; set; } = new T(); |
|
||||
|
|
||||
public Guid Key { get; private set; } |
|
||||
|
|
||||
protected IPersistence<T> Persistence |
|
||||
{ |
|
||||
get { return persistence; } |
|
||||
} |
|
||||
|
|
||||
protected GrainOfGuid(IStore<Guid> store) |
|
||||
{ |
|
||||
Guard.NotNull(store, nameof(store)); |
|
||||
|
|
||||
this.store = store; |
|
||||
} |
|
||||
|
|
||||
public sealed override Task OnActivateAsync() |
|
||||
{ |
|
||||
return ActivateAsync(this.GetPrimaryKey()); |
|
||||
} |
|
||||
|
|
||||
public async Task ActivateAsync(Guid key) |
|
||||
{ |
|
||||
Key = key; |
|
||||
|
|
||||
persistence = store.WithSnapshots(GetType(), key, new HandleSnapshot<T>(ApplyState)); |
|
||||
|
|
||||
await persistence.ReadAsync(); |
|
||||
|
|
||||
await OnActivateAsync(key); |
|
||||
} |
|
||||
|
|
||||
protected virtual Task OnActivateAsync(Guid key) |
|
||||
{ |
|
||||
return TaskHelper.Done; |
|
||||
} |
|
||||
|
|
||||
private void ApplyState(T state) |
|
||||
{ |
|
||||
State = state; |
|
||||
} |
|
||||
|
|
||||
public Task ClearStateAsync() |
|
||||
{ |
|
||||
State = new T(); |
|
||||
|
|
||||
return persistence.DeleteAsync(); |
|
||||
} |
|
||||
|
|
||||
protected Task WriteStateAsync() |
|
||||
{ |
|
||||
return persistence.WriteSnapshotAsync(State); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,74 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using System.Threading.Tasks; |
|
||||
using Orleans; |
|
||||
using Squidex.Infrastructure.States; |
|
||||
using Squidex.Infrastructure.Tasks; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Orleans |
|
||||
{ |
|
||||
public abstract class GrainOfString<T> : Grain where T : class, new() |
|
||||
{ |
|
||||
private readonly IStore<string> store; |
|
||||
private IPersistence<T> persistence; |
|
||||
|
|
||||
public string Key { get; set; } |
|
||||
|
|
||||
protected T State { get; set; } = new T(); |
|
||||
|
|
||||
protected IPersistence<T> Persistence |
|
||||
{ |
|
||||
get { return persistence; } |
|
||||
} |
|
||||
|
|
||||
protected GrainOfString(IStore<string> store) |
|
||||
{ |
|
||||
Guard.NotNull(store, nameof(store)); |
|
||||
|
|
||||
this.store = store; |
|
||||
} |
|
||||
|
|
||||
public sealed override Task OnActivateAsync() |
|
||||
{ |
|
||||
return ActivateAsync(this.GetPrimaryKeyString()); |
|
||||
} |
|
||||
|
|
||||
public async Task ActivateAsync(string key) |
|
||||
{ |
|
||||
Key = key; |
|
||||
|
|
||||
persistence = store.WithSnapshots(GetType(), key, new HandleSnapshot<T>(ApplyState)); |
|
||||
|
|
||||
await persistence.ReadAsync(); |
|
||||
|
|
||||
await OnActivateAsync(key); |
|
||||
} |
|
||||
|
|
||||
protected virtual Task OnActivateAsync(string key) |
|
||||
{ |
|
||||
return TaskHelper.Done; |
|
||||
} |
|
||||
|
|
||||
private void ApplyState(T state) |
|
||||
{ |
|
||||
State = state; |
|
||||
} |
|
||||
|
|
||||
public Task ClearStateAsync() |
|
||||
{ |
|
||||
State = new T(); |
|
||||
|
|
||||
return persistence.DeleteAsync(); |
|
||||
} |
|
||||
|
|
||||
protected Task WriteStateAsync() |
|
||||
{ |
|
||||
return persistence.WriteSnapshotAsync(State); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,85 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Microsoft.Extensions.DependencyInjection; |
||||
|
using Orleans; |
||||
|
using Orleans.Runtime; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
using Squidex.Infrastructure.States; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Orleans |
||||
|
{ |
||||
|
public sealed class GrainState<T> : IGrainState<T> where T : class, new() |
||||
|
{ |
||||
|
private readonly IGrainActivationContext context; |
||||
|
private IPersistence<T> persistence; |
||||
|
|
||||
|
public T Value { get; set; } = new T(); |
||||
|
|
||||
|
public long Version |
||||
|
{ |
||||
|
get { return persistence.Version; } |
||||
|
} |
||||
|
|
||||
|
public GrainState(IGrainActivationContext context) |
||||
|
{ |
||||
|
Guard.NotNull(context, nameof(context)); |
||||
|
|
||||
|
this.context = context; |
||||
|
|
||||
|
context.ObservableLifecycle.Subscribe("Persistence", GrainLifecycleStage.SetupState, SetupAsync); |
||||
|
} |
||||
|
|
||||
|
public Task SetupAsync(CancellationToken ct = default) |
||||
|
{ |
||||
|
if (ct.IsCancellationRequested) |
||||
|
{ |
||||
|
return Task.CompletedTask; |
||||
|
} |
||||
|
|
||||
|
if (context.GrainIdentity.PrimaryKeyString != null) |
||||
|
{ |
||||
|
var store = context.ActivationServices.GetService<IStore<string>>(); |
||||
|
|
||||
|
persistence = store.WithSnapshots<T>(GetType(), context.GrainIdentity.PrimaryKeyString, ApplyState); |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
var store = context.ActivationServices.GetService<IStore<Guid>>(); |
||||
|
|
||||
|
persistence = store.WithSnapshots<T>(GetType(), context.GrainIdentity.PrimaryKey, ApplyState); |
||||
|
} |
||||
|
|
||||
|
return persistence.ReadAsync(); |
||||
|
} |
||||
|
|
||||
|
private void ApplyState(T value) |
||||
|
{ |
||||
|
Value = value; |
||||
|
} |
||||
|
|
||||
|
public Task ClearAsync() |
||||
|
{ |
||||
|
Value = new T(); |
||||
|
|
||||
|
return persistence.DeleteAsync(); |
||||
|
} |
||||
|
|
||||
|
public Task WriteAsync() |
||||
|
{ |
||||
|
return persistence.WriteSnapshotAsync(Value); |
||||
|
} |
||||
|
|
||||
|
public Task WriteEventAsync(Envelope<IEvent> envelope) |
||||
|
{ |
||||
|
return persistence.WriteEventAsync(envelope); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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.Orleans |
||||
|
{ |
||||
|
public interface IActivationLimit |
||||
|
{ |
||||
|
void SetLimit(int maxActivations, TimeSpan lifetime); |
||||
|
|
||||
|
void ReportIAmAlive(); |
||||
|
|
||||
|
void ReportIAmDead(); |
||||
|
} |
||||
|
} |
||||
@ -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.Orleans |
||||
|
{ |
||||
|
public interface IActivationLimiter |
||||
|
{ |
||||
|
void Register(Type grainType, IDeactivater deactivater, int maxActivations); |
||||
|
|
||||
|
void Unregister(Type grainType, IDeactivater deactivater); |
||||
|
} |
||||
|
} |
||||
@ -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.Concurrency; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Orleans |
||||
|
{ |
||||
|
public interface IDeactivatableGrain |
||||
|
{ |
||||
|
[AlwaysInterleave] |
||||
|
[OneWay] |
||||
|
Task DeactivateAsync(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
namespace Squidex.Infrastructure.Orleans |
||||
|
{ |
||||
|
public interface IDeactivater |
||||
|
{ |
||||
|
void Deactivate(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,25 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System.Threading.Tasks; |
||||
|
using Squidex.Infrastructure.EventSourcing; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Orleans |
||||
|
{ |
||||
|
public interface IGrainState<T> where T : class, new() |
||||
|
{ |
||||
|
long Version { get; } |
||||
|
|
||||
|
T Value { get; set; } |
||||
|
|
||||
|
Task ClearAsync(); |
||||
|
|
||||
|
Task WriteAsync(); |
||||
|
|
||||
|
Task WriteEventAsync(Envelope<IEvent> envelope); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Linq; |
||||
|
using FluentAssertions; |
||||
|
using Squidex.Domain.Apps.Core.Contents; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Domain.Apps.Core.Model.Contents |
||||
|
{ |
||||
|
public class ContentFieldDataTests |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void Should_serialize_and_deserialize() |
||||
|
{ |
||||
|
var fieldData = |
||||
|
new ContentFieldData() |
||||
|
.AddValue(12); |
||||
|
|
||||
|
var serialized = fieldData.SerializeAndDeserialize(); |
||||
|
|
||||
|
serialized.Should().BeEquivalentTo(fieldData); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_intern_invariant_key() |
||||
|
{ |
||||
|
var fieldData = |
||||
|
new ContentFieldData() |
||||
|
.AddValue(12); |
||||
|
|
||||
|
var serialized = fieldData.SerializeAndDeserialize(); |
||||
|
|
||||
|
Assert.NotNull(string.IsInterned(serialized.Keys.First())); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_intern_known_language() |
||||
|
{ |
||||
|
var fieldData = |
||||
|
new ContentFieldData() |
||||
|
.AddValue("en", 12); |
||||
|
|
||||
|
var serialized = fieldData.SerializeAndDeserialize(); |
||||
|
|
||||
|
Assert.NotNull(string.IsInterned(serialized.Keys.First())); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_intern_unknown_key() |
||||
|
{ |
||||
|
var fieldData = |
||||
|
new ContentFieldData() |
||||
|
.AddValue(Guid.NewGuid().ToString(), 12); |
||||
|
|
||||
|
var serialized = fieldData.SerializeAndDeserialize(); |
||||
|
|
||||
|
Assert.Null(string.IsInterned(serialized.Keys.First())); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,83 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 Orleans; |
||||
|
using Orleans.Runtime; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Orleans |
||||
|
{ |
||||
|
public class ActivationLimiterFilterTests |
||||
|
{ |
||||
|
private readonly IIncomingGrainCallContext context = A.Fake<IIncomingGrainCallContext>(); |
||||
|
private readonly ActivationLimiterFilter sut; |
||||
|
|
||||
|
public ActivationLimiterFilterTests() |
||||
|
{ |
||||
|
sut = new ActivationLimiterFilter(); |
||||
|
} |
||||
|
|
||||
|
public sealed class MyGrain : GrainBase |
||||
|
{ |
||||
|
public MyGrain(IActivationLimit limit) |
||||
|
: base(null, CreateRuntime(limit)) |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
private static IGrainRuntime CreateRuntime(IActivationLimit limit) |
||||
|
{ |
||||
|
var serviceProvider = A.Fake<IServiceProvider>(); |
||||
|
|
||||
|
var grainRuntime = A.Fake<IGrainRuntime>(); |
||||
|
|
||||
|
A.CallTo(() => grainRuntime.ServiceProvider) |
||||
|
.Returns(serviceProvider); |
||||
|
|
||||
|
A.CallTo(() => serviceProvider.GetService(typeof(IActivationLimit))) |
||||
|
.Returns(limit); |
||||
|
|
||||
|
return grainRuntime; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_update_iam_alive_for_grain_base() |
||||
|
{ |
||||
|
var limit = A.Fake<IActivationLimit>(); |
||||
|
|
||||
|
var grain = new MyGrain(limit); |
||||
|
|
||||
|
A.CallTo(() => context.Grain) |
||||
|
.Returns(grain); |
||||
|
|
||||
|
await sut.Invoke(context); |
||||
|
|
||||
|
A.CallTo(() => limit.ReportIAmAlive()) |
||||
|
.MustHaveHappened(); |
||||
|
|
||||
|
A.CallTo(() => context.Invoke()) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task Should_also_handle_other_grains() |
||||
|
{ |
||||
|
var grain = A.Fake<Grain>(); |
||||
|
|
||||
|
A.CallTo(() => context.Grain) |
||||
|
.Returns(grain); |
||||
|
|
||||
|
await sut.Invoke(context); |
||||
|
|
||||
|
A.CallTo(() => context.Invoke()) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,98 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using FakeItEasy; |
||||
|
using Orleans; |
||||
|
using Orleans.Core; |
||||
|
using Orleans.Runtime; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Squidex.Infrastructure.Orleans |
||||
|
{ |
||||
|
public class ActivationLimiterTests |
||||
|
{ |
||||
|
private readonly IGrainIdentity grainIdentity = A.Fake<IGrainIdentity>(); |
||||
|
private readonly IGrainRuntime grainRuntime = A.Fake<IGrainRuntime>(); |
||||
|
private readonly ActivationLimiter sut; |
||||
|
|
||||
|
private class MyGrain : GrainBase |
||||
|
{ |
||||
|
public MyGrain(IGrainIdentity identity, IGrainRuntime runtime, IActivationLimit limit) |
||||
|
: base(identity, runtime) |
||||
|
{ |
||||
|
limit.SetLimit(3, TimeSpan.FromMinutes(3)); |
||||
|
Limit = limit; |
||||
|
} |
||||
|
|
||||
|
public IActivationLimit Limit { get; } |
||||
|
} |
||||
|
|
||||
|
public ActivationLimiterTests() |
||||
|
{ |
||||
|
sut = new ActivationLimiter(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_deactivate_last_grain() |
||||
|
{ |
||||
|
var grain1 = CreateGuidGrain(); |
||||
|
|
||||
|
CreateGuidGrain(); |
||||
|
CreateGuidGrain(); |
||||
|
CreateGuidGrain(); |
||||
|
|
||||
|
A.CallTo(() => grainRuntime.DeactivateOnIdle(grain1)) |
||||
|
.MustHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Should_not_deactivate_last_grain_if_other_died() |
||||
|
{ |
||||
|
CreateGuidGrain(); |
||||
|
CreateGuidGrain().ReportIAmDead(); |
||||
|
CreateGuidGrain(); |
||||
|
CreateGuidGrain(); |
||||
|
|
||||
|
A.CallTo(() => grainRuntime.DeactivateOnIdle(A<Grain>.Ignored)) |
||||
|
.MustNotHaveHappened(); |
||||
|
} |
||||
|
|
||||
|
private MyGrain CreateGuidGrain() |
||||
|
{ |
||||
|
var context = A.Fake<IGrainActivationContext>(); |
||||
|
|
||||
|
var limit = new ActivationLimit(context, sut); |
||||
|
|
||||
|
var serviceProvider = A.Fake<IServiceProvider>(); |
||||
|
|
||||
|
A.CallTo(() => grainRuntime.ServiceProvider) |
||||
|
.Returns(serviceProvider); |
||||
|
|
||||
|
A.CallTo(() => context.ActivationServices) |
||||
|
.Returns(serviceProvider); |
||||
|
|
||||
|
A.CallTo(() => serviceProvider.GetService(typeof(IActivationLimit))) |
||||
|
.Returns(limit); |
||||
|
|
||||
|
A.CallTo(() => serviceProvider.GetService(typeof(IGrainRuntime))) |
||||
|
.Returns(grainRuntime); |
||||
|
|
||||
|
var grain = new MyGrain(grainIdentity, grainRuntime, limit); |
||||
|
|
||||
|
A.CallTo(() => context.GrainInstance) |
||||
|
.Returns(grain); |
||||
|
|
||||
|
A.CallTo(() => context.GrainType) |
||||
|
.Returns(typeof(MyGrain)); |
||||
|
|
||||
|
grain.ReportIAmAlive(); |
||||
|
|
||||
|
return grain; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,101 +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 Squidex.Infrastructure.States; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Orleans |
|
||||
{ |
|
||||
public class GrainOfGuidTests |
|
||||
{ |
|
||||
private readonly IPersistence<MyGrain.GrainState> persistence = A.Fake<IPersistence<MyGrain.GrainState>>(); |
|
||||
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>(); |
|
||||
private readonly Guid id = Guid.NewGuid(); |
|
||||
private readonly MyGrain sut; |
|
||||
private HandleSnapshot<MyGrain.GrainState> read; |
|
||||
|
|
||||
public sealed class MyGrain : GrainOfGuid<MyGrain.GrainState> |
|
||||
{ |
|
||||
public sealed class GrainState |
|
||||
{ |
|
||||
public Guid Id { get; set; } |
|
||||
} |
|
||||
|
|
||||
public GrainState PublicState |
|
||||
{ |
|
||||
get { return State; } |
|
||||
} |
|
||||
|
|
||||
public MyGrain(IStore<Guid> store) |
|
||||
: base(store) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public Task PublicWriteAsync() |
|
||||
{ |
|
||||
return WriteStateAsync(); |
|
||||
} |
|
||||
|
|
||||
public Task PublicClearAsync() |
|
||||
{ |
|
||||
return ClearStateAsync(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public GrainOfGuidTests() |
|
||||
{ |
|
||||
A.CallTo(() => persistence.ReadAsync(EtagVersion.Any)) |
|
||||
.Invokes(_ => |
|
||||
{ |
|
||||
read(new MyGrain.GrainState { Id = id }); |
|
||||
}); |
|
||||
|
|
||||
A.CallTo(() => store.WithSnapshots(typeof(MyGrain), id, A<HandleSnapshot<MyGrain.GrainState>>.Ignored)) |
|
||||
.Invokes(new Action<Type, Guid, HandleSnapshot<MyGrain.GrainState>>((type, id, callback) => |
|
||||
{ |
|
||||
read = callback; |
|
||||
})) |
|
||||
.Returns(persistence); |
|
||||
|
|
||||
sut = new MyGrain(store); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_read_on_activate() |
|
||||
{ |
|
||||
await sut.ActivateAsync(id); |
|
||||
|
|
||||
Assert.Equal(id, sut.PublicState.Id); |
|
||||
|
|
||||
A.CallTo(() => persistence.ReadAsync(EtagVersion.Any)) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_invoke_persistence_on_write() |
|
||||
{ |
|
||||
await sut.ActivateAsync(id); |
|
||||
await sut.PublicWriteAsync(); |
|
||||
|
|
||||
A.CallTo(() => persistence.WriteSnapshotAsync(sut.PublicState)) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_invoke_persistence_on_clear() |
|
||||
{ |
|
||||
await sut.ActivateAsync(id); |
|
||||
await sut.PublicClearAsync(); |
|
||||
|
|
||||
A.CallTo(() => persistence.DeleteAsync()) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,101 +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 Squidex.Infrastructure.States; |
|
||||
using Xunit; |
|
||||
|
|
||||
namespace Squidex.Infrastructure.Orleans |
|
||||
{ |
|
||||
public class GrainOfStringTests |
|
||||
{ |
|
||||
private readonly IPersistence<MyGrain.GrainState> persistence = A.Fake<IPersistence<MyGrain.GrainState>>(); |
|
||||
private readonly IStore<string> store = A.Fake<IStore<string>>(); |
|
||||
private readonly string id = Guid.NewGuid().ToString(); |
|
||||
private readonly MyGrain sut; |
|
||||
private HandleSnapshot<MyGrain.GrainState> read; |
|
||||
|
|
||||
public sealed class MyGrain : GrainOfString<MyGrain.GrainState> |
|
||||
{ |
|
||||
public sealed class GrainState |
|
||||
{ |
|
||||
public string Id { get; set; } |
|
||||
} |
|
||||
|
|
||||
public GrainState PublicState |
|
||||
{ |
|
||||
get { return State; } |
|
||||
} |
|
||||
|
|
||||
public MyGrain(IStore<string> store) |
|
||||
: base(store) |
|
||||
{ |
|
||||
} |
|
||||
|
|
||||
public Task PublicWriteAsync() |
|
||||
{ |
|
||||
return WriteStateAsync(); |
|
||||
} |
|
||||
|
|
||||
public Task PublicClearAsync() |
|
||||
{ |
|
||||
return ClearStateAsync(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
public GrainOfStringTests() |
|
||||
{ |
|
||||
A.CallTo(() => persistence.ReadAsync(EtagVersion.Any)) |
|
||||
.Invokes(_ => |
|
||||
{ |
|
||||
read(new MyGrain.GrainState { Id = id }); |
|
||||
}); |
|
||||
|
|
||||
A.CallTo(() => store.WithSnapshots(typeof(MyGrain), id, A<HandleSnapshot<MyGrain.GrainState>>.Ignored)) |
|
||||
.Invokes(new Action<Type, string, HandleSnapshot<MyGrain.GrainState>>((type, id, callback) => |
|
||||
{ |
|
||||
read = callback; |
|
||||
})) |
|
||||
.Returns(persistence); |
|
||||
|
|
||||
sut = new MyGrain(store); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_read_on_activate() |
|
||||
{ |
|
||||
await sut.ActivateAsync(id); |
|
||||
|
|
||||
Assert.Equal(id, sut.PublicState.Id); |
|
||||
|
|
||||
A.CallTo(() => persistence.ReadAsync(EtagVersion.Any)) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_invoke_persistence_on_write() |
|
||||
{ |
|
||||
await sut.ActivateAsync(id); |
|
||||
await sut.PublicWriteAsync(); |
|
||||
|
|
||||
A.CallTo(() => persistence.WriteSnapshotAsync(sut.PublicState)) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
|
|
||||
[Fact] |
|
||||
public async Task Should_invoke_persistence_on_clear() |
|
||||
{ |
|
||||
await sut.ActivateAsync(id); |
|
||||
await sut.PublicClearAsync(); |
|
||||
|
|
||||
A.CallTo(() => persistence.DeleteAsync()) |
|
||||
.MustHaveHappened(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -1,75 +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 Squidex.ClientLibrary; |
|
||||
using Squidex.ClientLibrary.Management; |
|
||||
|
|
||||
namespace LoadTest |
|
||||
{ |
|
||||
public sealed class ClientQueryFixture : IDisposable |
|
||||
{ |
|
||||
public SquidexClient<TestEntity, TestEntityData> Client { get; } = TestClient.Build(); |
|
||||
|
|
||||
public ClientQueryFixture() |
|
||||
{ |
|
||||
Task.Run(async () => |
|
||||
{ |
|
||||
var apps = TestClient.ClientManager.CreateAppsClient(); |
|
||||
|
|
||||
try |
|
||||
{ |
|
||||
await apps.PostAppAsync(new CreateAppDto |
|
||||
{ |
|
||||
Name = TestClient.TestAppName |
|
||||
}); |
|
||||
|
|
||||
var schemas = TestClient.ClientManager.CreateSchemasClient(); |
|
||||
|
|
||||
await schemas.PostSchemaAsync(TestClient.TestAppName, new CreateSchemaDto |
|
||||
{ |
|
||||
Name = TestClient.TestSchemaName, |
|
||||
Fields = new List<UpsertSchemaFieldDto> |
|
||||
{ |
|
||||
new UpsertSchemaFieldDto |
|
||||
{ |
|
||||
Name = TestClient.TestSchemaField, |
|
||||
Properties = new NumberFieldPropertiesDto() |
|
||||
} |
|
||||
}, |
|
||||
IsPublished = true |
|
||||
}); |
|
||||
} |
|
||||
catch (SquidexManagementException ex) |
|
||||
{ |
|
||||
if (ex.StatusCode != 400) |
|
||||
{ |
|
||||
throw; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
var contents = await Client.GetAllAsync(); |
|
||||
|
|
||||
foreach (var content in contents.Items) |
|
||||
{ |
|
||||
await Client.DeleteAsync(content); |
|
||||
} |
|
||||
|
|
||||
for (var i = 10; i > 0; i--) |
|
||||
{ |
|
||||
await Client.CreateAsync(new TestEntityData { Value = i }, true); |
|
||||
} |
|
||||
}).Wait(); |
|
||||
} |
|
||||
|
|
||||
public void Dispose() |
|
||||
{ |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,95 @@ |
|||||
|
// ==========================================================================
|
||||
|
// 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 Squidex.ClientLibrary; |
||||
|
using Squidex.ClientLibrary.Management; |
||||
|
|
||||
|
namespace LoadTest.Model |
||||
|
{ |
||||
|
public static class TestClient |
||||
|
{ |
||||
|
public const string ServerUrl = "http://localhost:5000"; |
||||
|
|
||||
|
public const string ClientId = "root"; |
||||
|
public const string ClientSecret = "xeLd6jFxqbXJrfmNLlO2j1apagGGGSyZJhFnIuHp4I0="; |
||||
|
|
||||
|
public const string TestAppName = "integration-tests"; |
||||
|
|
||||
|
public static readonly SquidexClientManager ClientManager = |
||||
|
new SquidexClientManager( |
||||
|
ServerUrl, |
||||
|
TestAppName, |
||||
|
ClientId, |
||||
|
ClientSecret) |
||||
|
{ |
||||
|
ReadResponseAsString = true |
||||
|
}; |
||||
|
|
||||
|
public static async Task<SquidexClient<TestEntity, TestEntityData>> BuildAsync(string schemaName) |
||||
|
{ |
||||
|
await CreateAppIfNotExistsAsync(); |
||||
|
await CreateSchemaIfNotExistsAsync(schemaName); |
||||
|
|
||||
|
return ClientManager.GetClient<TestEntity, TestEntityData>(schemaName); |
||||
|
} |
||||
|
|
||||
|
private static async Task CreateAppIfNotExistsAsync() |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var apps = ClientManager.CreateAppsClient(); |
||||
|
|
||||
|
await apps.PostAppAsync(new CreateAppDto |
||||
|
{ |
||||
|
Name = TestAppName |
||||
|
}); |
||||
|
} |
||||
|
catch (SquidexManagementException ex) |
||||
|
{ |
||||
|
if (ex.StatusCode != 400) |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private static async Task CreateSchemaIfNotExistsAsync(string schemaName) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var schemas = ClientManager.CreateSchemasClient(); |
||||
|
|
||||
|
await schemas.PostSchemaAsync(TestAppName, new CreateSchemaDto |
||||
|
{ |
||||
|
Name = schemaName, |
||||
|
Fields = new List<UpsertSchemaFieldDto> |
||||
|
{ |
||||
|
new UpsertSchemaFieldDto |
||||
|
{ |
||||
|
Name = "value", |
||||
|
Properties = new NumberFieldPropertiesDto |
||||
|
{ |
||||
|
IsRequired = true, |
||||
|
IsListField = true |
||||
|
} |
||||
|
} |
||||
|
}, |
||||
|
IsPublished = true |
||||
|
}); |
||||
|
} |
||||
|
catch (SquidexManagementException ex) |
||||
|
{ |
||||
|
if (ex.StatusCode != 400) |
||||
|
{ |
||||
|
throw; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using LoadTest.Model; |
||||
|
using Squidex.ClientLibrary; |
||||
|
|
||||
|
namespace LoadTest |
||||
|
{ |
||||
|
public sealed class ReadingFixture : IDisposable |
||||
|
{ |
||||
|
public SquidexClient<TestEntity, TestEntityData> Client { get; private set; } |
||||
|
|
||||
|
public ReadingFixture() |
||||
|
{ |
||||
|
Task.Run(async () => |
||||
|
{ |
||||
|
Client = await TestClient.BuildAsync("reading"); |
||||
|
|
||||
|
var contents = await Client.GetAllAsync(); |
||||
|
|
||||
|
if (contents.Total != 10) |
||||
|
{ |
||||
|
foreach (var content in contents.Items) |
||||
|
{ |
||||
|
await Client.DeleteAsync(content); |
||||
|
} |
||||
|
|
||||
|
for (var i = 10; i > 0; i--) |
||||
|
{ |
||||
|
await Client.CreateAsync(new TestEntityData { Value = i }, true); |
||||
|
} |
||||
|
} |
||||
|
}).Wait(); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -1,34 +0,0 @@ |
|||||
// ==========================================================================
|
|
||||
// Squidex Headless CMS
|
|
||||
// ==========================================================================
|
|
||||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|
||||
// All rights reserved. Licensed under the MIT license.
|
|
||||
// ==========================================================================
|
|
||||
|
|
||||
using Squidex.ClientLibrary; |
|
||||
|
|
||||
namespace LoadTest |
|
||||
{ |
|
||||
public static class TestClient |
|
||||
{ |
|
||||
public const string ServerUrl = "http://localhost:5000"; |
|
||||
|
|
||||
public const string ClientId = "root"; |
|
||||
public const string ClientSecret = "xeLd6jFxqbXJrfmNLlO2j1apagGGGSyZJhFnIuHp4I0="; |
|
||||
|
|
||||
public const string TestAppName = "integration-tests"; |
|
||||
public const string TestSchemaName = "numbers"; |
|
||||
public const string TestSchemaField = "value"; |
|
||||
|
|
||||
public static readonly SquidexClientManager ClientManager = |
|
||||
new SquidexClientManager("http://localhost:5000", TestAppName, ClientId, ClientSecret) |
|
||||
{ |
|
||||
ReadResponseAsString = true |
|
||||
}; |
|
||||
|
|
||||
public static SquidexClient<TestEntity, TestEntityData> Build() |
|
||||
{ |
|
||||
return ClientManager.GetClient<TestEntity, TestEntityData>(TestSchemaName); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
@ -0,0 +1,73 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Collections.Concurrent; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Diagnostics; |
||||
|
using System.Linq; |
||||
|
using System.Threading; |
||||
|
using System.Threading.Tasks; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace LoadTest.Utils |
||||
|
{ |
||||
|
public static class Run |
||||
|
{ |
||||
|
public static async Task Parallel(int numUsers, int numIterationsPerUser, Func<Task> action, int expectedAvg = 100) |
||||
|
{ |
||||
|
var elapsedMs = new ConcurrentBag<long>(); |
||||
|
|
||||
|
var errors = 0; |
||||
|
|
||||
|
async Task RunAsync() |
||||
|
{ |
||||
|
for (var i = 0; i < numIterationsPerUser; i++) |
||||
|
{ |
||||
|
try |
||||
|
{ |
||||
|
var watch = Stopwatch.StartNew(); |
||||
|
|
||||
|
await action(); |
||||
|
|
||||
|
watch.Stop(); |
||||
|
|
||||
|
elapsedMs.Add(watch.ElapsedMilliseconds); |
||||
|
} |
||||
|
catch |
||||
|
{ |
||||
|
Interlocked.Increment(ref errors); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
var tasks = new List<Task>(); |
||||
|
|
||||
|
for (var i = 0; i < numUsers; i++) |
||||
|
{ |
||||
|
tasks.Add(Task.Run(RunAsync)); |
||||
|
} |
||||
|
|
||||
|
await Task.WhenAll(tasks); |
||||
|
|
||||
|
var count = elapsedMs.Count; |
||||
|
|
||||
|
var max = elapsedMs.Max(); |
||||
|
var min = elapsedMs.Min(); |
||||
|
|
||||
|
var avg = elapsedMs.Average(); |
||||
|
|
||||
|
Assert.Equal(0, errors); |
||||
|
Assert.Equal(count, numUsers * numIterationsPerUser); |
||||
|
|
||||
|
Assert.InRange(max, 0, expectedAvg * 10); |
||||
|
Assert.InRange(min, 0, expectedAvg); |
||||
|
|
||||
|
Assert.InRange(avg, 0, expectedAvg); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,31 @@ |
|||||
|
// ==========================================================================
|
||||
|
// Squidex Headless CMS
|
||||
|
// ==========================================================================
|
||||
|
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
||||
|
// All rights reserved. Licensed under the MIT license.
|
||||
|
// ==========================================================================
|
||||
|
|
||||
|
using System; |
||||
|
using System.Threading.Tasks; |
||||
|
using LoadTest.Model; |
||||
|
using Squidex.ClientLibrary; |
||||
|
|
||||
|
namespace LoadTest |
||||
|
{ |
||||
|
public sealed class WritingFixture : IDisposable |
||||
|
{ |
||||
|
public SquidexClient<TestEntity, TestEntityData> Client { get; private set; } |
||||
|
|
||||
|
public WritingFixture() |
||||
|
{ |
||||
|
Task.Run(async () => |
||||
|
{ |
||||
|
Client = await TestClient.BuildAsync("reading"); |
||||
|
}).Wait(); |
||||
|
} |
||||
|
|
||||
|
public void Dispose() |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue