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