Browse Source

Feature/performance improvements (#404)

* 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
Sebastian Stehle 6 years ago
committed by GitHub
parent
commit
649d35fc19
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 17
      src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs
  2. 65
      src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs
  3. 2
      src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs
  4. 22
      src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs
  5. 34
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs
  6. 25
      src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs
  7. 12
      src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs
  8. 31
      src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs
  9. 20
      src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs
  10. 12
      src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs
  11. 27
      src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs
  12. 2
      src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs
  13. 27
      src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs
  14. 29
      src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs
  15. 2
      src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs
  16. 34
      src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs
  17. 15
      src/Squidex.Domain.Apps.Entities/SquidexEventEnricher.cs
  18. 37
      src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs
  19. 10
      src/Squidex.Infrastructure/Caching/LRUCache.cs
  20. 12
      src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs
  21. 22
      src/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs
  22. 34
      src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs
  23. 14
      src/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs
  24. 77
      src/Squidex.Infrastructure/Orleans/ActivationLimit.cs
  25. 72
      src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs
  26. 25
      src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs
  27. 63
      src/Squidex.Infrastructure/Orleans/GrainBase.cs
  28. 19
      src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs
  29. 75
      src/Squidex.Infrastructure/Orleans/GrainOfGuid{T}.cs
  30. 25
      src/Squidex.Infrastructure/Orleans/GrainOfString.cs
  31. 74
      src/Squidex.Infrastructure/Orleans/GrainOfString{T}.cs
  32. 85
      src/Squidex.Infrastructure/Orleans/GrainState.cs
  33. 20
      src/Squidex.Infrastructure/Orleans/IActivationLimit.cs
  34. 18
      src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs
  35. 19
      src/Squidex.Infrastructure/Orleans/IDeactivatableGrain.cs
  36. 14
      src/Squidex.Infrastructure/Orleans/IDeactivater.cs
  37. 25
      src/Squidex.Infrastructure/Orleans/IGrainState.cs
  38. 3
      src/Squidex.Infrastructure/States/Persistence.cs
  39. 8
      src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs
  40. 7
      src/Squidex.Infrastructure/States/Store.cs
  41. 2
      src/Squidex/Config/Domain/EntitiesServices.cs
  42. 1
      src/Squidex/Config/Domain/SerializationServices.cs
  43. 10
      src/Squidex/Config/Orleans/OrleansServices.cs
  44. 66
      tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs
  45. 1
      tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs
  46. 9
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs
  47. 15
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs
  48. 18
      tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs
  49. 3
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs
  50. 11
      tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs
  51. 11
      tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs
  52. 19
      tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs
  53. 19
      tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs
  54. 13
      tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs
  55. 18
      tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs
  56. 84
      tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs
  57. 35
      tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs
  58. 83
      tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs
  59. 98
      tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs
  60. 101
      tests/Squidex.Infrastructure.Tests/Orleans/GrainOfGuidTests.cs
  61. 101
      tests/Squidex.Infrastructure.Tests/Orleans/GrainOfStringTests.cs
  62. 5
      tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs
  63. 2
      tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs
  64. 75
      tools/LoadTest/ClientQueryFixture.cs
  65. 95
      tools/LoadTest/Model/TestClient.cs
  66. 2
      tools/LoadTest/Model/TestEntity.cs
  67. 30
      tools/LoadTest/ReadingBenchmarks.cs
  68. 46
      tools/LoadTest/ReadingFixture.cs
  69. 34
      tools/LoadTest/TestClient.cs
  70. 73
      tools/LoadTest/Utils/Run.cs
  71. 30
      tools/LoadTest/WritingBenchmarks.cs
  72. 31
      tools/LoadTest/WritingFixture.cs

17
src/Squidex.Domain.Apps.Core.Model/Contents/ContentFieldData.cs

@ -21,7 +21,7 @@ namespace Squidex.Domain.Apps.Core.Contents
public ContentFieldData AddValue(object value) public ContentFieldData AddValue(object value)
{ {
return AddJsonValue(InvariantPartitioning.Key, JsonValue.Create(value)); return AddJsonValue(JsonValue.Create(value));
} }
public ContentFieldData AddValue(string key, object value) public ContentFieldData AddValue(string key, object value)
@ -29,11 +29,26 @@ namespace Squidex.Domain.Apps.Core.Contents
return AddJsonValue(key, JsonValue.Create(value)); return AddJsonValue(key, JsonValue.Create(value));
} }
public ContentFieldData AddJsonValue(IJsonValue value)
{
this[InvariantPartitioning.Key] = value;
return this;
}
public ContentFieldData AddJsonValue(string key, IJsonValue value) public ContentFieldData AddJsonValue(string key, IJsonValue value)
{ {
Guard.NotNullOrEmpty(key, nameof(key)); Guard.NotNullOrEmpty(key, nameof(key));
if (Language.IsValidLanguage(key))
{
this[key] = value; this[key] = value;
// this[string.Intern(key)] = value;
}
else
{
this[key] = value;
}
return this; return this;
} }

65
src/Squidex.Domain.Apps.Core.Model/Contents/Json/ContentFieldDataConverter.cs

@ -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.");
}
}
}

2
src/Squidex.Domain.Apps.Entities/Apps/AppGrain.cs

@ -26,7 +26,7 @@ using Squidex.Shared.Users;
namespace Squidex.Domain.Apps.Entities.Apps namespace Squidex.Domain.Apps.Entities.Apps
{ {
public sealed class AppGrain : SquidexDomainObjectGrain<AppState>, IAppGrain public sealed class AppGrain : DomainObjectGrain<AppState>, IAppGrain
{ {
private readonly InitialPatterns initialPatterns; private readonly InitialPatterns initialPatterns;
private readonly IAppPlansProvider appPlansProvider; private readonly IAppPlansProvider appPlansProvider;

22
src/Squidex.Domain.Apps.Entities/Apps/AppUISettingsGrain.cs

@ -15,29 +15,33 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Apps namespace Squidex.Domain.Apps.Entities.Apps
{ {
public sealed class AppUISettingsGrain : GrainOfGuid<AppUISettingsGrain.GrainState>, IAppUISettingsGrain public sealed class AppUISettingsGrain : GrainOfGuid, IAppUISettingsGrain
{ {
private readonly IGrainState<GrainState> state;
[CollectionName("UISettings")] [CollectionName("UISettings")]
public sealed class GrainState public sealed class GrainState
{ {
public JsonObject Settings { get; set; } = JsonValue.Object(); public JsonObject Settings { get; set; } = JsonValue.Object();
} }
public AppUISettingsGrain(IStore<Guid> store) public AppUISettingsGrain(IGrainState<GrainState> state)
: base(store)
{ {
Guard.NotNull(state, nameof(state));
this.state = state;
} }
public Task<J<JsonObject>> GetAsync() public Task<J<JsonObject>> GetAsync()
{ {
return Task.FromResult(State.Settings.AsJ()); return Task.FromResult(state.Value.Settings.AsJ());
} }
public Task SetAsync(J<JsonObject> settings) public Task SetAsync(J<JsonObject> settings)
{ {
State.Settings = settings; state.Value.Settings = settings;
return WriteStateAsync(); return state.WriteAsync();
} }
public Task SetAsync(string path, J<IJsonValue> value) public Task SetAsync(string path, J<IJsonValue> value)
@ -51,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
container[key] = value.Value; container[key] = value.Value;
return WriteStateAsync(); return state.WriteAsync();
} }
public Task RemoveAsync(string path) public Task RemoveAsync(string path)
@ -63,7 +67,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
container.Remove(key); container.Remove(key);
} }
return WriteStateAsync(); return state.WriteAsync();
} }
private JsonObject GetContainer(string path, out string key) private JsonObject GetContainer(string path, out string key)
@ -74,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Apps
key = segments[segments.Length - 1]; key = segments[segments.Length - 1];
var current = State.Settings; var current = state.Value.Settings;
if (segments.Length > 1) if (segments.Length > 1)
{ {

34
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByNameIndexGrain.cs

@ -9,16 +9,18 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Apps.Indexes namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{ {
public sealed class AppsByNameIndexGrain : GrainOfString<AppsByNameIndexGrain.GrainState>, IAppsByNameIndex public sealed class AppsByNameIndexGrain : GrainOfString, IAppsByNameIndex
{ {
private readonly HashSet<Guid> reservedIds = new HashSet<Guid>(); private readonly HashSet<Guid> reservedIds = new HashSet<Guid>();
private readonly HashSet<string> reservedNames = new HashSet<string>(); private readonly HashSet<string> reservedNames = new HashSet<string>();
private readonly IGrainState<GrainState> state;
[CollectionName("Index_AppsByName")] [CollectionName("Index_AppsByName")]
public sealed class GrainState public sealed class GrainState
@ -26,16 +28,18 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
public Dictionary<string, Guid> Apps { get; set; } = new Dictionary<string, Guid>(StringComparer.Ordinal); public Dictionary<string, Guid> Apps { get; set; } = new Dictionary<string, Guid>(StringComparer.Ordinal);
} }
public AppsByNameIndexGrain(IStore<string> store) public AppsByNameIndexGrain(IGrainState<GrainState> state)
: base(store)
{ {
Guard.NotNull(state, nameof(state));
this.state = state;
} }
public Task RebuildAsync(Dictionary<string, Guid> apps) public Task RebuildAsync(Dictionary<string, Guid> apps)
{ {
State = new GrainState { Apps = apps }; state.Value = new GrainState { Apps = apps };
return WriteStateAsync(); return state.WriteAsync();
} }
public Task<bool> ReserveAppAsync(Guid appId, string name) public Task<bool> ReserveAppAsync(Guid appId, string name)
@ -53,7 +57,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
private bool IsInUse(Guid appId, string name) private bool IsInUse(Guid appId, string name)
{ {
return State.Apps.ContainsKey(name) || State.Apps.Any(x => x.Value == appId); return state.Value.Apps.ContainsKey(name) || state.Value.Apps.Any(x => x.Value == appId);
} }
private bool IsReserved(Guid appId, string name) private bool IsReserved(Guid appId, string name)
@ -71,27 +75,27 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
public Task AddAppAsync(Guid appId, string name) public Task AddAppAsync(Guid appId, string name)
{ {
State.Apps[name] = appId; state.Value.Apps[name] = appId;
reservedIds.Remove(appId); reservedIds.Remove(appId);
reservedNames.Remove(name); reservedNames.Remove(name);
return WriteStateAsync(); return state.WriteAsync();
} }
public Task RemoveAppAsync(Guid appId) public Task RemoveAppAsync(Guid appId)
{ {
var name = State.Apps.FirstOrDefault(x => x.Value == appId).Key; var name = state.Value.Apps.FirstOrDefault(x => x.Value == appId).Key;
if (!string.IsNullOrWhiteSpace(name)) if (!string.IsNullOrWhiteSpace(name))
{ {
State.Apps.Remove(name); state.Value.Apps.Remove(name);
reservedIds.Remove(appId); reservedIds.Remove(appId);
reservedNames.Remove(name); reservedNames.Remove(name);
} }
return WriteStateAsync(); return state.WriteAsync();
} }
public Task<List<Guid>> GetAppIdsAsync(params string[] names) public Task<List<Guid>> GetAppIdsAsync(params string[] names)
@ -100,7 +104,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
foreach (var appName in names) foreach (var appName in names)
{ {
if (State.Apps.TryGetValue(appName, out var appId)) if (state.Value.Apps.TryGetValue(appName, out var appId))
{ {
appIds.Add(appId); appIds.Add(appId);
} }
@ -111,19 +115,19 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
public Task<Guid> GetAppIdAsync(string appName) public Task<Guid> GetAppIdAsync(string appName)
{ {
State.Apps.TryGetValue(appName, out var appId); state.Value.Apps.TryGetValue(appName, out var appId);
return Task.FromResult(appId); return Task.FromResult(appId);
} }
public Task<List<Guid>> GetAppIdsAsync() public Task<List<Guid>> GetAppIdsAsync()
{ {
return Task.FromResult(State.Apps.Values.ToList()); return Task.FromResult(state.Value.Apps.Values.ToList());
} }
public Task<long> CountAsync() public Task<long> CountAsync()
{ {
return Task.FromResult((long)State.Apps.Count); return Task.FromResult((long)state.Value.Apps.Count);
} }
} }
} }

25
src/Squidex.Domain.Apps.Entities/Apps/Indexes/AppsByUserIndexGrain.cs

@ -9,48 +9,53 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Apps.Indexes namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{ {
public sealed class AppsByUserIndexGrain : GrainOfString<AppsByUserIndexGrain.GrainState>, IAppsByUserIndex public sealed class AppsByUserIndexGrain : GrainOfString, IAppsByUserIndex
{ {
private readonly IGrainState<GrainState> state;
[CollectionName("Index_AppsByUser")] [CollectionName("Index_AppsByUser")]
public sealed class GrainState public sealed class GrainState
{ {
public HashSet<Guid> Apps { get; set; } = new HashSet<Guid>(); public HashSet<Guid> Apps { get; set; } = new HashSet<Guid>();
} }
public AppsByUserIndexGrain(IStore<string> store) public AppsByUserIndexGrain(IGrainState<GrainState> state)
: base(store)
{ {
Guard.NotNull(state, nameof(state));
this.state = state;
} }
public Task RebuildAsync(HashSet<Guid> apps) public Task RebuildAsync(HashSet<Guid> apps)
{ {
State = new GrainState { Apps = apps }; state.Value = new GrainState { Apps = apps };
return WriteStateAsync(); return state.WriteAsync();
} }
public Task AddAppAsync(Guid appId) public Task AddAppAsync(Guid appId)
{ {
State.Apps.Add(appId); state.Value.Apps.Add(appId);
return WriteStateAsync(); return state.WriteAsync();
} }
public Task RemoveAppAsync(Guid appId) public Task RemoveAppAsync(Guid appId)
{ {
State.Apps.Remove(appId); state.Value.Apps.Remove(appId);
return WriteStateAsync(); return state.WriteAsync();
} }
public Task<List<Guid>> GetAppIdsAsync() public Task<List<Guid>> GetAppIdsAsync()
{ {
return Task.FromResult(State.Apps.ToList()); return Task.FromResult(state.Value.Apps.ToList());
} }
} }
} }

12
src/Squidex.Domain.Apps.Entities/Assets/AssetGrain.cs

@ -24,24 +24,26 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
{ {
public sealed class AssetGrain : SquidexDomainObjectGrainLogSnapshots<AssetState>, IAssetGrain public sealed class AssetGrain : LogSnapshotDomainObjectGrain<AssetState>, IAssetGrain
{ {
private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5);
private readonly ITagService tagService; private readonly ITagService tagService;
public AssetGrain(IStore<Guid> store, ITagService tagService, ISemanticLog log) public AssetGrain(IStore<Guid> store, ITagService tagService, IActivationLimit limit, ISemanticLog log)
: base(store, log) : base(store, log)
{ {
Guard.NotNull(tagService, nameof(tagService)); Guard.NotNull(tagService, nameof(tagService));
this.tagService = tagService; this.tagService = tagService;
limit?.SetLimit(5000, Lifetime);
} }
public override Task OnActivateAsync() protected override Task OnActivateAsync(Guid key)
{ {
DelayDeactivation(Lifetime); TryDelayDeactivation(Lifetime);
return base.OnActivateAsync(); return base.OnActivateAsync(key);
} }
protected override Task<object> ExecuteAsync(IAggregateCommand command) protected override Task<object> ExecuteAsync(IAggregateCommand command)

31
src/Squidex.Domain.Apps.Entities/Backup/BackupGrain.cs

@ -22,13 +22,12 @@ using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Json; using Squidex.Infrastructure.Json;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Backup namespace Squidex.Domain.Apps.Entities.Backup
{ {
[Reentrant] [Reentrant]
public sealed class BackupGrain : GrainOfGuid<BackupState>, IBackupGrain public sealed class BackupGrain : GrainOfGuid, IBackupGrain
{ {
private const int MaxBackups = 10; private const int MaxBackups = 10;
private static readonly Duration UpdateDuration = Duration.FromSeconds(1); private static readonly Duration UpdateDuration = Duration.FromSeconds(1);
@ -40,6 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
private readonly IEventDataFormatter eventDataFormatter; private readonly IEventDataFormatter eventDataFormatter;
private readonly IEventStore eventStore; private readonly IEventStore eventStore;
private readonly ISemanticLog log; private readonly ISemanticLog log;
private readonly IGrainState<BackupState> state;
private CancellationTokenSource currentTask; private CancellationTokenSource currentTask;
private BackupStateJob currentJob; private BackupStateJob currentJob;
@ -52,8 +52,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
IJsonSerializer serializer, IJsonSerializer serializer,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ISemanticLog log, ISemanticLog log,
IStore<Guid> store) IGrainState<BackupState> state)
: base(store)
{ {
Guard.NotNull(assetStore, nameof(assetStore)); Guard.NotNull(assetStore, nameof(assetStore));
Guard.NotNull(backupArchiveLocation, nameof(backupArchiveLocation)); Guard.NotNull(backupArchiveLocation, nameof(backupArchiveLocation));
@ -62,6 +61,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter));
Guard.NotNull(serviceProvider, nameof(serviceProvider)); Guard.NotNull(serviceProvider, nameof(serviceProvider));
Guard.NotNull(serializer, nameof(serializer)); Guard.NotNull(serializer, nameof(serializer));
Guard.NotNull(state, nameof(state));
Guard.NotNull(log, nameof(log)); Guard.NotNull(log, nameof(log));
this.assetStore = assetStore; this.assetStore = assetStore;
@ -71,6 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
this.eventDataFormatter = eventDataFormatter; this.eventDataFormatter = eventDataFormatter;
this.serializer = serializer; this.serializer = serializer;
this.serviceProvider = serviceProvider; this.serviceProvider = serviceProvider;
this.state = state;
this.log = log; this.log = log;
} }
@ -83,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
private async Task RecoverAfterRestartAsync() private async Task RecoverAfterRestartAsync()
{ {
foreach (var job in State.Jobs) foreach (var job in state.Value.Jobs)
{ {
if (!job.Stopped.HasValue) if (!job.Stopped.HasValue)
{ {
@ -96,7 +97,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
job.Status = JobStatus.Failed; job.Status = JobStatus.Failed;
await WriteStateAsync(); await state.WriteAsync();
} }
} }
} }
@ -108,7 +109,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
throw new DomainException("Another backup process is already running."); throw new DomainException("Another backup process is already running.");
} }
if (State.Jobs.Count >= MaxBackups) if (state.Value.Jobs.Count >= MaxBackups)
{ {
throw new DomainException($"You cannot have more than {MaxBackups} backups."); throw new DomainException($"You cannot have more than {MaxBackups} backups.");
} }
@ -123,9 +124,9 @@ namespace Squidex.Domain.Apps.Entities.Backup
currentTask = new CancellationTokenSource(); currentTask = new CancellationTokenSource();
currentJob = job; currentJob = job;
State.Jobs.Insert(0, job); state.Value.Jobs.Insert(0, job);
await WriteStateAsync(); await state.WriteAsync();
Process(job, currentTask.Token); Process(job, currentTask.Token);
} }
@ -201,7 +202,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
job.Stopped = clock.GetCurrentInstant(); job.Stopped = clock.GetCurrentInstant();
await WriteStateAsync(); await state.WriteAsync();
currentTask = null; currentTask = null;
currentJob = null; currentJob = null;
@ -216,7 +217,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
{ {
lastTimestamp = now; lastTimestamp = now;
await WriteStateAsync(); await state.WriteAsync();
} }
return lastTimestamp; return lastTimestamp;
@ -224,7 +225,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
public async Task DeleteAsync(Guid id) public async Task DeleteAsync(Guid id)
{ {
var job = State.Jobs.FirstOrDefault(x => x.Id == id); var job = state.Value.Jobs.FirstOrDefault(x => x.Id == id);
if (job == null) if (job == null)
{ {
@ -242,9 +243,9 @@ namespace Squidex.Domain.Apps.Entities.Backup
await Safe.DeleteAsync(backupArchiveLocation, jobId, log); await Safe.DeleteAsync(backupArchiveLocation, jobId, log);
await Safe.DeleteAsync(assetStore, jobId, log); await Safe.DeleteAsync(assetStore, jobId, log);
State.Jobs.Remove(job); state.Value.Jobs.Remove(job);
await WriteStateAsync(); await state.WriteAsync();
} }
} }
@ -255,7 +256,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
public Task<J<List<IBackupJob>>> GetStateAsync() public Task<J<List<IBackupJob>>> GetStateAsync()
{ {
return J.AsTask(State.Jobs.OfType<IBackupJob>().ToList()); return J.AsTask(state.Value.Jobs.OfType<IBackupJob>().ToList());
} }
} }
} }

20
src/Squidex.Domain.Apps.Entities/Backup/RestoreGrain.cs

@ -27,7 +27,7 @@ using Squidex.Infrastructure.Tasks;
namespace Squidex.Domain.Apps.Entities.Backup namespace Squidex.Domain.Apps.Entities.Backup
{ {
public sealed class RestoreGrain : GrainOfString<RestoreState>, IRestoreGrain public sealed class RestoreGrain : GrainOfString, IRestoreGrain
{ {
private readonly IBackupArchiveLocation backupArchiveLocation; private readonly IBackupArchiveLocation backupArchiveLocation;
private readonly IClock clock; private readonly IClock clock;
@ -38,10 +38,11 @@ namespace Squidex.Domain.Apps.Entities.Backup
private readonly ISemanticLog log; private readonly ISemanticLog log;
private readonly IServiceProvider serviceProvider; private readonly IServiceProvider serviceProvider;
private readonly IStreamNameResolver streamNameResolver; private readonly IStreamNameResolver streamNameResolver;
private readonly IGrainState<RestoreState> state;
private RestoreStateJob CurrentJob private RestoreStateJob CurrentJob
{ {
get { return State.Job; } get { return state.Value.Job; }
} }
public RestoreGrain(IBackupArchiveLocation backupArchiveLocation, public RestoreGrain(IBackupArchiveLocation backupArchiveLocation,
@ -53,8 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
ISemanticLog log, ISemanticLog log,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
IStreamNameResolver streamNameResolver, IStreamNameResolver streamNameResolver,
IStore<string> store) IGrainState<RestoreState> state)
: base(store)
{ {
Guard.NotNull(backupArchiveLocation, nameof(backupArchiveLocation)); Guard.NotNull(backupArchiveLocation, nameof(backupArchiveLocation));
Guard.NotNull(clock, nameof(clock)); Guard.NotNull(clock, nameof(clock));
@ -63,7 +63,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter));
Guard.NotNull(serializer, nameof(serializer)); Guard.NotNull(serializer, nameof(serializer));
Guard.NotNull(serviceProvider, nameof(serviceProvider)); Guard.NotNull(serviceProvider, nameof(serviceProvider));
Guard.NotNull(store, nameof(store)); Guard.NotNull(state, nameof(state));
Guard.NotNull(streamNameResolver, nameof(streamNameResolver)); Guard.NotNull(streamNameResolver, nameof(streamNameResolver));
Guard.NotNull(log, nameof(log)); Guard.NotNull(log, nameof(log));
@ -75,6 +75,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
this.serializer = serializer; this.serializer = serializer;
this.serviceProvider = serviceProvider; this.serviceProvider = serviceProvider;
this.streamNameResolver = streamNameResolver; this.streamNameResolver = streamNameResolver;
this.state = state;
this.log = log; this.log = log;
} }
@ -96,7 +97,8 @@ namespace Squidex.Domain.Apps.Entities.Backup
CurrentJob.Status = JobStatus.Failed; CurrentJob.Status = JobStatus.Failed;
await CleanupAsync(handlers); await CleanupAsync(handlers);
await WriteStateAsync();
await state.WriteAsync();
} }
} }
@ -115,7 +117,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
throw new DomainException("A restore operation is already running."); throw new DomainException("A restore operation is already running.");
} }
State.Job = new RestoreStateJob state.Value.Job = new RestoreStateJob
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
NewAppName = newAppName, NewAppName = newAppName,
@ -125,7 +127,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
Url = url Url = url
}; };
await WriteStateAsync(); await state.WriteAsync();
Process(); Process();
} }
@ -235,7 +237,7 @@ namespace Squidex.Domain.Apps.Entities.Backup
{ {
CurrentJob.Stopped = clock.GetCurrentInstant(); CurrentJob.Stopped = clock.GetCurrentInstant();
await WriteStateAsync(); await state.WriteAsync();
} }
} }
} }

12
src/Squidex.Domain.Apps.Entities/Contents/ContentGrain.cs

@ -26,7 +26,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
{ {
public sealed class ContentGrain : SquidexDomainObjectGrainLogSnapshots<ContentState>, IContentGrain public sealed class ContentGrain : LogSnapshotDomainObjectGrain<ContentState>, IContentGrain
{ {
private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5); private static readonly TimeSpan Lifetime = TimeSpan.FromMinutes(5);
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
@ -42,7 +42,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
IAssetRepository assetRepository, IAssetRepository assetRepository,
IScriptEngine scriptEngine, IScriptEngine scriptEngine,
IContentWorkflow contentWorkflow, IContentWorkflow contentWorkflow,
IContentRepository contentRepository) IContentRepository contentRepository,
IActivationLimit limit)
: base(store, log) : base(store, log)
{ {
Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(appProvider, nameof(appProvider));
@ -56,13 +57,8 @@ namespace Squidex.Domain.Apps.Entities.Contents
this.assetRepository = assetRepository; this.assetRepository = assetRepository;
this.contentWorkflow = contentWorkflow; this.contentWorkflow = contentWorkflow;
this.contentRepository = contentRepository; this.contentRepository = contentRepository;
}
public override Task OnActivateAsync()
{
DelayDeactivation(Lifetime);
return base.OnActivateAsync(); limit?.SetLimit(5000, Lifetime);
} }
protected override Task<object> ExecuteAsync(IAggregateCommand command) protected override Task<object> ExecuteAsync(IAggregateCommand command)

27
src/Squidex.Domain.Apps.Entities/Rules/Indexes/RulesByAppIndexGrain.cs

@ -9,53 +9,58 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Rules.Indexes namespace Squidex.Domain.Apps.Entities.Rules.Indexes
{ {
public sealed class RulesByAppIndexGrain : GrainOfGuid<RulesByAppIndexGrain.GrainState>, IRulesByAppIndex public sealed class RulesByAppIndexGrain : GrainOfGuid, IRulesByAppIndex
{ {
private readonly IGrainState<GrainState> state;
[CollectionName("Index_RulesByApp")] [CollectionName("Index_RulesByApp")]
public sealed class GrainState public sealed class GrainState
{ {
public HashSet<Guid> Rules { get; set; } = new HashSet<Guid>(); public HashSet<Guid> Rules { get; set; } = new HashSet<Guid>();
} }
public RulesByAppIndexGrain(IStore<Guid> store) public RulesByAppIndexGrain(IGrainState<GrainState> state)
: base(store)
{ {
Guard.NotNull(state, nameof(state));
this.state = state;
} }
public Task ClearAsync() public Task ClearAsync()
{ {
return ClearStateAsync(); return state.ClearAsync();
} }
public Task RebuildAsync(HashSet<Guid> rules) public Task RebuildAsync(HashSet<Guid> rules)
{ {
State = new GrainState { Rules = rules }; state.Value = new GrainState { Rules = rules };
return WriteStateAsync(); return state.WriteAsync();
} }
public Task AddRuleAsync(Guid ruleId) public Task AddRuleAsync(Guid ruleId)
{ {
State.Rules.Add(ruleId); state.Value.Rules.Add(ruleId);
return WriteStateAsync(); return state.WriteAsync();
} }
public Task RemoveRuleAsync(Guid ruleId) public Task RemoveRuleAsync(Guid ruleId)
{ {
State.Rules.Remove(ruleId); state.Value.Rules.Remove(ruleId);
return WriteStateAsync(); return state.WriteAsync();
} }
public Task<List<Guid>> GetRuleIdsAsync() public Task<List<Guid>> GetRuleIdsAsync()
{ {
return Task.FromResult(State.Rules.ToList()); return Task.FromResult(state.Value.Rules.ToList());
} }
} }
} }

2
src/Squidex.Domain.Apps.Entities/Rules/RuleGrain.cs

@ -22,7 +22,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Rules namespace Squidex.Domain.Apps.Entities.Rules
{ {
public sealed class RuleGrain : SquidexDomainObjectGrain<RuleState>, IRuleGrain public sealed class RuleGrain : DomainObjectGrain<RuleState>, IRuleGrain
{ {
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;

27
src/Squidex.Domain.Apps.Entities/Rules/UsageTracking/UsageTrackerGrain.cs

@ -22,8 +22,9 @@ using Squidex.Infrastructure.UsageTracking;
namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
{ {
[Reentrant] [Reentrant]
public sealed class UsageTrackerGrain : GrainOfString<UsageTrackerGrain.GrainState>, IRemindable, IUsageTrackerGrain public sealed class UsageTrackerGrain : GrainOfString, IRemindable, IUsageTrackerGrain
{ {
private readonly IGrainState<GrainState> state;
private readonly IUsageTracker usageTracker; private readonly IUsageTracker usageTracker;
public sealed class Target public sealed class Target
@ -43,11 +44,13 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
public Dictionary<Guid, Target> Targets { get; set; } = new Dictionary<Guid, Target>(); public Dictionary<Guid, Target> Targets { get; set; } = new Dictionary<Guid, Target>();
} }
public UsageTrackerGrain(IStore<string> store, IUsageTracker usageTracker) public UsageTrackerGrain(IGrainState<GrainState> state, IUsageTracker usageTracker)
: base(store)
{ {
Guard.NotNull(state, nameof(state));
Guard.NotNull(usageTracker, nameof(usageTracker)); Guard.NotNull(usageTracker, nameof(usageTracker));
this.state = state;
this.usageTracker = usageTracker; this.usageTracker = usageTracker;
} }
@ -75,7 +78,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
{ {
var today = DateTime.Today; var today = DateTime.Today;
foreach (var kvp in State.Targets) foreach (var kvp in state.Value.Targets)
{ {
var target = kvp.Value; var target = kvp.Value;
@ -99,12 +102,12 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
RuleId = kvp.Key RuleId = kvp.Key
}; };
await Persistence.WriteEventAsync(Envelope.Create<IEvent>(@event)); await state.WriteEventAsync(Envelope.Create<IEvent>(@event));
} }
} }
} }
await WriteStateAsync(); await state.WriteAsync();
} }
private static DateTime GetFromDate(DateTime today, int? numDays) private static DateTime GetFromDate(DateTime today, int? numDays)
@ -123,33 +126,33 @@ namespace Squidex.Domain.Apps.Entities.Rules.UsageTracking
{ {
UpdateTarget(ruleId, t => { t.Limits = limits; t.AppId = appId; t.NumDays = numDays; }); UpdateTarget(ruleId, t => { t.Limits = limits; t.AppId = appId; t.NumDays = numDays; });
return WriteStateAsync(); return state.WriteAsync();
} }
public Task UpdateTargetAsync(Guid ruleId, int limits, int? numDays) public Task UpdateTargetAsync(Guid ruleId, int limits, int? numDays)
{ {
UpdateTarget(ruleId, t => { t.Limits = limits; t.NumDays = numDays; }); UpdateTarget(ruleId, t => { t.Limits = limits; t.NumDays = numDays; });
return WriteStateAsync(); return state.WriteAsync();
} }
public Task AddTargetAsync(Guid ruleId, int limits) public Task AddTargetAsync(Guid ruleId, int limits)
{ {
UpdateTarget(ruleId, t => t.Limits = limits); UpdateTarget(ruleId, t => t.Limits = limits);
return WriteStateAsync(); return state.WriteAsync();
} }
public Task RemoveTargetAsync(Guid ruleId) public Task RemoveTargetAsync(Guid ruleId)
{ {
State.Targets.Remove(ruleId); state.Value.Targets.Remove(ruleId);
return WriteStateAsync(); return state.WriteAsync();
} }
private void UpdateTarget(Guid ruleId, Action<Target> updater) private void UpdateTarget(Guid ruleId, Action<Target> updater)
{ {
updater(State.Targets.GetOrAddNew(ruleId)); updater(state.Value.Targets.GetOrAddNew(ruleId));
} }
} }
} }

29
src/Squidex.Domain.Apps.Entities/Schemas/Indexes/SchemasByAppIndexGrain.cs

@ -9,60 +9,65 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Schemas.Indexes namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
{ {
public sealed class SchemasByAppIndexGrain : GrainOfGuid<SchemasByAppIndexGrain.GrainState>, ISchemasByAppIndex public sealed class SchemasByAppIndexGrain : GrainOfGuid, ISchemasByAppIndex
{ {
private readonly IGrainState<GrainState> state;
[CollectionName("Index_SchemasByApp")] [CollectionName("Index_SchemasByApp")]
public sealed class GrainState public sealed class GrainState
{ {
public Dictionary<string, Guid> Schemas { get; set; } = new Dictionary<string, Guid>(); public Dictionary<string, Guid> Schemas { get; set; } = new Dictionary<string, Guid>();
} }
public SchemasByAppIndexGrain(IStore<Guid> store) public SchemasByAppIndexGrain(IGrainState<GrainState> state)
: base(store)
{ {
Guard.NotNull(state, nameof(state));
this.state = state;
} }
public Task ClearAsync() public Task ClearAsync()
{ {
return ClearStateAsync(); return state.ClearAsync();
} }
public Task RebuildAsync(Dictionary<string, Guid> schemas) public Task RebuildAsync(Dictionary<string, Guid> schemas)
{ {
State = new GrainState { Schemas = schemas }; state.Value = new GrainState { Schemas = schemas };
return WriteStateAsync(); return state.WriteAsync();
} }
public Task AddSchemaAsync(Guid schemaId, string name) public Task AddSchemaAsync(Guid schemaId, string name)
{ {
State.Schemas[name] = schemaId; state.Value.Schemas[name] = schemaId;
return WriteStateAsync(); return state.WriteAsync();
} }
public Task RemoveSchemaAsync(Guid schemaId) public Task RemoveSchemaAsync(Guid schemaId)
{ {
State.Schemas.Remove(State.Schemas.FirstOrDefault(x => x.Value == schemaId).Key ?? string.Empty); state.Value.Schemas.Remove(state.Value.Schemas.FirstOrDefault(x => x.Value == schemaId).Key ?? string.Empty);
return WriteStateAsync(); return state.WriteAsync();
} }
public Task<Guid> GetSchemaIdAsync(string name) public Task<Guid> GetSchemaIdAsync(string name)
{ {
State.Schemas.TryGetValue(name, out var schemaId); state.Value.Schemas.TryGetValue(name, out var schemaId);
return Task.FromResult(schemaId); return Task.FromResult(schemaId);
} }
public Task<List<Guid>> GetSchemaIdsAsync() public Task<List<Guid>> GetSchemaIdsAsync()
{ {
return Task.FromResult(State.Schemas.Values.ToList()); return Task.FromResult(state.Value.Schemas.Values.ToList());
} }
} }
} }

2
src/Squidex.Domain.Apps.Entities/Schemas/SchemaGrain.cs

@ -25,7 +25,7 @@ using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Schemas namespace Squidex.Domain.Apps.Entities.Schemas
{ {
public sealed class SchemaGrain : SquidexDomainObjectGrain<SchemaState>, ISchemaGrain public sealed class SchemaGrain : DomainObjectGrain<SchemaState>, ISchemaGrain
{ {
private readonly IAppProvider appProvider; private readonly IAppProvider appProvider;
private readonly IJsonSerializer serializer; private readonly IJsonSerializer serializer;

34
src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrainLogSnapshots.cs

@ -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);
}
}
}

15
src/Squidex.Domain.Apps.Entities/SquidexDomainObjectGrain.cs → src/Squidex.Domain.Apps.Entities/SquidexEventEnricher.cs

@ -5,30 +5,21 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using Squidex.Domain.Apps.Events; using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing; using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities namespace Squidex.Domain.Apps.Entities
{ {
public abstract class SquidexDomainObjectGrain<T> : DomainObjectGrain<T> where T : IDomainState<T>, new() public sealed class SquidexEventEnricher<T> : DefaultEventEnricher<T>
{ {
protected SquidexDomainObjectGrain(IStore<Guid> store, ISemanticLog log) public override void Enrich(Envelope<IEvent> @event, T id)
: base(store, log)
{
}
public override void RaiseEvent(Envelope<IEvent> @event)
{ {
if (@event.Payload is AppEvent appEvent) if (@event.Payload is AppEvent appEvent)
{ {
@event.SetAppId(appEvent.AppId.Id); @event.SetAppId(appEvent.AppId.Id);
} }
base.RaiseEvent(@event); base.Enrich(@event, id);
} }
} }
} }

37
src/Squidex.Domain.Apps.Entities/Tags/TagGrain.cs

@ -10,34 +10,39 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.States;
namespace Squidex.Domain.Apps.Entities.Tags namespace Squidex.Domain.Apps.Entities.Tags
{ {
public sealed class TagGrain : GrainOfString<TagGrain.GrainState>, ITagGrain public sealed class TagGrain : GrainOfString, ITagGrain
{ {
private readonly IGrainState<GrainState> state;
[CollectionName("Index_Tags")] [CollectionName("Index_Tags")]
public sealed class GrainState public sealed class GrainState
{ {
public TagsExport Tags { get; set; } = new TagsExport(); public TagsExport Tags { get; set; } = new TagsExport();
} }
public TagGrain(IStore<string> store) public TagGrain(IGrainState<GrainState> state)
: base(store)
{ {
Guard.NotNull(state, nameof(state));
this.state = state;
} }
public Task ClearAsync() public Task ClearAsync()
{ {
return ClearStateAsync(); return state.ClearAsync();
} }
public Task RebuildAsync(TagsExport tags) public Task RebuildAsync(TagsExport tags)
{ {
State.Tags = tags; state.Value.Tags = tags;
return WriteStateAsync(); return state.WriteAsync();
} }
public async Task<Dictionary<string, string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids) public async Task<Dictionary<string, string>> NormalizeTagsAsync(HashSet<string> names, HashSet<string> ids)
@ -53,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Tags
var tagName = tag.ToLowerInvariant(); var tagName = tag.ToLowerInvariant();
var tagId = string.Empty; var tagId = string.Empty;
var found = State.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, tagName, StringComparison.OrdinalIgnoreCase)); var found = state.Value.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, tagName, StringComparison.OrdinalIgnoreCase));
if (found.Value != null) if (found.Value != null)
{ {
@ -68,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Tags
{ {
tagId = Guid.NewGuid().ToString(); tagId = Guid.NewGuid().ToString();
State.Tags.Add(tagId, new Tag { Name = tagName }); state.Value.Tags.Add(tagId, new Tag { Name = tagName });
} }
result.Add(tagName, tagId); result.Add(tagName, tagId);
@ -82,20 +87,20 @@ namespace Squidex.Domain.Apps.Entities.Tags
{ {
if (!result.ContainsValue(id)) if (!result.ContainsValue(id))
{ {
if (State.Tags.TryGetValue(id, out var tagInfo)) if (state.Value.Tags.TryGetValue(id, out var tagInfo))
{ {
tagInfo.Count--; tagInfo.Count--;
if (tagInfo.Count <= 0) if (tagInfo.Count <= 0)
{ {
State.Tags.Remove(id); state.Value.Tags.Remove(id);
} }
} }
} }
} }
} }
await WriteStateAsync(); await state.WriteAsync();
return result; return result;
} }
@ -106,7 +111,7 @@ namespace Squidex.Domain.Apps.Entities.Tags
foreach (var name in names) foreach (var name in names)
{ {
var id = State.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, name, StringComparison.OrdinalIgnoreCase)).Key; var id = state.Value.Tags.FirstOrDefault(x => string.Equals(x.Value.Name, name, StringComparison.OrdinalIgnoreCase)).Key;
if (!string.IsNullOrWhiteSpace(id)) if (!string.IsNullOrWhiteSpace(id))
{ {
@ -123,7 +128,7 @@ namespace Squidex.Domain.Apps.Entities.Tags
foreach (var id in ids) foreach (var id in ids)
{ {
if (State.Tags.TryGetValue(id, out var tagInfo)) if (state.Value.Tags.TryGetValue(id, out var tagInfo))
{ {
result[id] = tagInfo.Name; result[id] = tagInfo.Name;
} }
@ -134,14 +139,14 @@ namespace Squidex.Domain.Apps.Entities.Tags
public Task<TagsSet> GetTagsAsync() public Task<TagsSet> GetTagsAsync()
{ {
var tags = State.Tags.Values.ToDictionary(x => x.Name, x => x.Count); var tags = state.Value.Tags.Values.ToDictionary(x => x.Name, x => x.Count);
return Task.FromResult(new TagsSet(tags, Persistence.Version)); return Task.FromResult(new TagsSet(tags, state.Version));
} }
public Task<TagsExport> GetExportableTagsAsync() public Task<TagsExport> GetExportableTagsAsync()
{ {
return Task.FromResult(State.Tags); return Task.FromResult(state.Value.Tags);
} }
} }
} }

10
src/Squidex.Infrastructure/Caching/LRUCache.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System;
using System.Collections.Generic; using System.Collections.Generic;
namespace Squidex.Infrastructure.Caching namespace Squidex.Infrastructure.Caching
@ -14,10 +15,15 @@ namespace Squidex.Infrastructure.Caching
private readonly Dictionary<TKey, LinkedListNode<LRUCacheItem<TKey, TValue>>> cacheMap = new Dictionary<TKey, LinkedListNode<LRUCacheItem<TKey, TValue>>>(); private readonly Dictionary<TKey, LinkedListNode<LRUCacheItem<TKey, TValue>>> cacheMap = new Dictionary<TKey, LinkedListNode<LRUCacheItem<TKey, TValue>>>();
private readonly LinkedList<LRUCacheItem<TKey, TValue>> cacheHistory = new LinkedList<LRUCacheItem<TKey, TValue>>(); private readonly LinkedList<LRUCacheItem<TKey, TValue>> cacheHistory = new LinkedList<LRUCacheItem<TKey, TValue>>();
private readonly int capacity; private readonly int capacity;
private readonly Action<TKey, TValue> itemEvicted;
public LRUCache(int capacity) public LRUCache(int capacity, Action<TKey, TValue> itemEvicted = null)
{ {
Guard.GreaterThan(capacity, 0, nameof(capacity));
this.capacity = capacity; this.capacity = capacity;
this.itemEvicted = itemEvicted ?? new Action<TKey, TValue>((key, value) => { });
} }
public bool Set(TKey key, TValue value) public bool Set(TKey key, TValue value)
@ -88,6 +94,8 @@ namespace Squidex.Infrastructure.Caching
{ {
var node = cacheHistory.First; var node = cacheHistory.First;
itemEvicted(node.Value.Key, node.Value.Value);
cacheMap.Remove(node.Value.Key); cacheMap.Remove(node.Value.Key);
cacheHistory.RemoveFirst(); cacheHistory.RemoveFirst();
} }

12
src/Squidex.Infrastructure/Commands/DomainObjectGrainBase.cs

@ -47,7 +47,7 @@ namespace Squidex.Infrastructure.Commands
this.log = log; this.log = log;
} }
protected sealed override async Task OnActivateAsync(Guid key) protected override async Task OnActivateAsync(Guid key)
{ {
var logContext = (key: key.ToString(), name: GetType().Name); var logContext = (key: key.ToString(), name: GetType().Name);
@ -159,13 +159,7 @@ namespace Squidex.Infrastructure.Commands
if (mode == Mode.Update && Version < 0) if (mode == Mode.Update && Version < 0)
{ {
try TryDeactivateOnIdle();
{
DeactivateOnIdle();
}
catch (InvalidOperationException)
{
}
throw new DomainObjectNotFoundException(id.ToString(), GetType()); throw new DomainObjectNotFoundException(id.ToString(), GetType());
} }
@ -207,7 +201,7 @@ namespace Squidex.Infrastructure.Commands
} }
finally finally
{ {
uncomittedEvents.Clear(); ClearUncommittedEvents();
} }
} }

22
src/Squidex.Infrastructure/EventSourcing/DefaultEventEnricher.cs

@ -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);
}
}
}
}

34
src/Squidex.Infrastructure/EventSourcing/Grains/EventConsumerGrain.cs

@ -17,9 +17,10 @@ using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.EventSourcing.Grains namespace Squidex.Infrastructure.EventSourcing.Grains
{ {
public class EventConsumerGrain : GrainOfString<EventConsumerState>, IEventConsumerGrain public class EventConsumerGrain : GrainOfString, IEventConsumerGrain
{ {
private readonly EventConsumerFactory eventConsumerFactory; private readonly EventConsumerFactory eventConsumerFactory;
private readonly IGrainState<EventConsumerState> state;
private readonly IEventDataFormatter eventDataFormatter; private readonly IEventDataFormatter eventDataFormatter;
private readonly IEventStore eventStore; private readonly IEventStore eventStore;
private readonly ISemanticLog log; private readonly ISemanticLog log;
@ -29,20 +30,21 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
public EventConsumerGrain( public EventConsumerGrain(
EventConsumerFactory eventConsumerFactory, EventConsumerFactory eventConsumerFactory,
IStore<string> store, IGrainState<EventConsumerState> state,
IEventStore eventStore, IEventStore eventStore,
IEventDataFormatter eventDataFormatter, IEventDataFormatter eventDataFormatter,
ISemanticLog log) ISemanticLog log)
: base(store)
{ {
Guard.NotNull(eventStore, nameof(eventStore)); Guard.NotNull(eventStore, nameof(eventStore));
Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter)); Guard.NotNull(eventDataFormatter, nameof(eventDataFormatter));
Guard.NotNull(eventConsumerFactory, nameof(eventConsumerFactory)); Guard.NotNull(eventConsumerFactory, nameof(eventConsumerFactory));
Guard.NotNull(state, nameof(state));
Guard.NotNull(log, nameof(log)); Guard.NotNull(log, nameof(log));
this.eventStore = eventStore; this.eventStore = eventStore;
this.eventDataFormatter = eventDataFormatter; this.eventDataFormatter = eventDataFormatter;
this.eventConsumerFactory = eventConsumerFactory; this.eventConsumerFactory = eventConsumerFactory;
this.state = state;
this.log = log; this.log = log;
} }
@ -63,7 +65,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
private Immutable<EventConsumerInfo> CreateInfo() private Immutable<EventConsumerInfo> CreateInfo()
{ {
return State.ToInfo(eventConsumer.Name).AsImmutable(); return state.Value.ToInfo(eventConsumer.Name).AsImmutable();
} }
public Task OnEventAsync(Immutable<IEventSubscription> subscription, Immutable<StoredEvent> storedEvent) public Task OnEventAsync(Immutable<IEventSubscription> subscription, Immutable<StoredEvent> storedEvent)
@ -85,7 +87,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
} }
} }
State = State.Handled(storedEvent.Value.EventPosition); state.Value = state.Value.Handled(storedEvent.Value.EventPosition);
}); });
} }
@ -100,15 +102,15 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
{ {
Unsubscribe(); Unsubscribe();
State = State.Failed(exception.Value); state.Value = state.Value.Failed(exception.Value);
}); });
} }
public Task ActivateAsync() public Task ActivateAsync()
{ {
if (!State.IsStopped) if (!state.Value.IsStopped)
{ {
Subscribe(State.Position); Subscribe(state.Value.Position);
} }
return TaskHelper.Done; return TaskHelper.Done;
@ -116,16 +118,16 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
public async Task<Immutable<EventConsumerInfo>> StartAsync() public async Task<Immutable<EventConsumerInfo>> StartAsync()
{ {
if (!State.IsStopped) if (!state.Value.IsStopped)
{ {
return CreateInfo(); return CreateInfo();
} }
await DoAndUpdateStateAsync(() => await DoAndUpdateStateAsync(() =>
{ {
Subscribe(State.Position); Subscribe(state.Value.Position);
State = State.Started(); state.Value = state.Value.Started();
}); });
return CreateInfo(); return CreateInfo();
@ -133,7 +135,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
public async Task<Immutable<EventConsumerInfo>> StopAsync() public async Task<Immutable<EventConsumerInfo>> StopAsync()
{ {
if (State.IsStopped) if (state.Value.IsStopped)
{ {
return CreateInfo(); return CreateInfo();
} }
@ -142,7 +144,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
{ {
Unsubscribe(); Unsubscribe();
State = State.Stopped(); state.Value = state.Value.Stopped();
}); });
return CreateInfo(); return CreateInfo();
@ -158,7 +160,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
Subscribe(null); Subscribe(null);
State = State.Reset(); state.Value = state.Value.Reset();
}); });
return CreateInfo(); return CreateInfo();
@ -191,10 +193,10 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
.WriteProperty("status", "Failed") .WriteProperty("status", "Failed")
.WriteProperty("eventConsumer", eventConsumer.Name)); .WriteProperty("eventConsumer", eventConsumer.Name));
State = State.Failed(ex); state.Value = state.Value.Failed(ex);
} }
await WriteStateAsync(); await state.WriteAsync();
} }
private async Task ClearAsync() private async Task ClearAsync()

14
src/Squidex.Infrastructure/EventSourcing/IEventEnricher.cs

@ -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);
}
}

77
src/Squidex.Infrastructure/Orleans/ActivationLimit.cs

@ -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);
}
}
}

72
src/Squidex.Infrastructure/Orleans/ActivationLimiter.cs

@ -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);
}
}
}

25
src/Squidex.Infrastructure/Orleans/ActivationLimiterFilter.cs

@ -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();
}
}
}

63
src/Squidex.Infrastructure/Orleans/GrainBase.cs

@ -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)
{
}
}
}
}

19
src/Squidex.Infrastructure/Orleans/GrainOfGuid.cs

@ -8,24 +8,35 @@
using System; using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Orleans.Core;
using Orleans.Runtime;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Orleans namespace Squidex.Infrastructure.Orleans
{ {
public abstract class GrainOfGuid : Grain public abstract class GrainOfGuid : GrainBase
{ {
public Guid Key { get; private set; } public Guid Key { get; private set; }
public override Task OnActivateAsync() protected GrainOfGuid()
{
}
protected GrainOfGuid(IGrainIdentity identity, IGrainRuntime runtime)
: base(identity, runtime)
{
}
public sealed override Task OnActivateAsync()
{ {
return ActivateAsync(this.GetPrimaryKey()); return ActivateAsync(this.GetPrimaryKey());
} }
public Task ActivateAsync(Guid key) public async Task ActivateAsync(Guid key)
{ {
Key = key; Key = key;
return OnActivateAsync(key); await OnActivateAsync(key);
} }
protected virtual Task OnActivateAsync(Guid key) protected virtual Task OnActivateAsync(Guid key)

75
src/Squidex.Infrastructure/Orleans/GrainOfGuid{T}.cs

@ -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);
}
}
}

25
src/Squidex.Infrastructure/Orleans/GrainOfString.cs

@ -7,24 +7,41 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Orleans; using Orleans;
using Orleans.Core;
using Orleans.Runtime;
using Squidex.Infrastructure.Tasks; using Squidex.Infrastructure.Tasks;
namespace Squidex.Infrastructure.Orleans namespace Squidex.Infrastructure.Orleans
{ {
public abstract class GrainOfString : Grain public abstract class GrainOfString : GrainBase
{ {
public string Key { get; private set; } public string Key { get; private set; }
public override Task OnActivateAsync() protected GrainOfString()
{
}
protected GrainOfString(IGrainIdentity identity, IGrainRuntime runtime)
: base(identity, runtime)
{
}
public sealed override Task OnActivateAsync()
{ {
return ActivateAsync(this.GetPrimaryKeyString()); return ActivateAsync(this.GetPrimaryKeyString());
} }
public Task ActivateAsync(string key) public async Task ActivateAsync(string key)
{ {
Key = key; Key = key;
return OnActivateAsync(key); await OnLoadAsync(key);
await OnActivateAsync(key);
}
protected virtual Task OnLoadAsync(string key)
{
return TaskHelper.Done;
} }
protected virtual Task OnActivateAsync(string key) protected virtual Task OnActivateAsync(string key)

74
src/Squidex.Infrastructure/Orleans/GrainOfString{T}.cs

@ -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);
}
}
}

85
src/Squidex.Infrastructure/Orleans/GrainState.cs

@ -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);
}
}
}

20
src/Squidex.Infrastructure/Orleans/IActivationLimit.cs

@ -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();
}
}

18
src/Squidex.Infrastructure/Orleans/IActivationLimiter.cs

@ -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);
}
}

19
src/Squidex.Infrastructure/Orleans/IDeactivatableGrain.cs

@ -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();
}
}

14
src/Squidex.Infrastructure/Orleans/IDeactivater.cs

@ -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();
}
}

25
src/Squidex.Infrastructure/Orleans/IGrainState.cs

@ -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);
}
}

3
src/Squidex.Infrastructure/States/Persistence.cs

@ -14,11 +14,12 @@ namespace Squidex.Infrastructure.States
{ {
public Persistence(TKey ownerKey, Type ownerType, public Persistence(TKey ownerKey, Type ownerType,
IEventStore eventStore, IEventStore eventStore,
IEventEnricher<TKey> eventEnricher,
IEventDataFormatter eventDataFormatter, IEventDataFormatter eventDataFormatter,
ISnapshotStore<None, TKey> snapshotStore, ISnapshotStore<None, TKey> snapshotStore,
IStreamNameResolver streamNameResolver, IStreamNameResolver streamNameResolver,
HandleEvent applyEvent) HandleEvent applyEvent)
: base(ownerKey, ownerType, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent) : base(ownerKey, ownerType, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, PersistenceMode.EventSourcing, null, applyEvent)
{ {
} }
} }

8
src/Squidex.Infrastructure/States/Persistence{TSnapshot,TKey}.cs

@ -22,6 +22,7 @@ namespace Squidex.Infrastructure.States
private readonly ISnapshotStore<TSnapshot, TKey> snapshotStore; private readonly ISnapshotStore<TSnapshot, TKey> snapshotStore;
private readonly IStreamNameResolver streamNameResolver; private readonly IStreamNameResolver streamNameResolver;
private readonly IEventStore eventStore; private readonly IEventStore eventStore;
private readonly IEventEnricher<TKey> eventEnricher;
private readonly IEventDataFormatter eventDataFormatter; private readonly IEventDataFormatter eventDataFormatter;
private readonly PersistenceMode persistenceMode; private readonly PersistenceMode persistenceMode;
private readonly HandleSnapshot<TSnapshot> applyState; private readonly HandleSnapshot<TSnapshot> applyState;
@ -37,6 +38,7 @@ namespace Squidex.Infrastructure.States
public Persistence(TKey ownerKey, Type ownerType, public Persistence(TKey ownerKey, Type ownerType,
IEventStore eventStore, IEventStore eventStore,
IEventEnricher<TKey> eventEnricher,
IEventDataFormatter eventDataFormatter, IEventDataFormatter eventDataFormatter,
ISnapshotStore<TSnapshot, TKey> snapshotStore, ISnapshotStore<TSnapshot, TKey> snapshotStore,
IStreamNameResolver streamNameResolver, IStreamNameResolver streamNameResolver,
@ -49,6 +51,7 @@ namespace Squidex.Infrastructure.States
this.applyState = applyState; this.applyState = applyState;
this.applyEvent = applyEvent; this.applyEvent = applyEvent;
this.eventStore = eventStore; this.eventStore = eventStore;
this.eventEnricher = eventEnricher;
this.eventDataFormatter = eventDataFormatter; this.eventDataFormatter = eventDataFormatter;
this.persistenceMode = persistenceMode; this.persistenceMode = persistenceMode;
this.snapshotStore = snapshotStore; this.snapshotStore = snapshotStore;
@ -157,6 +160,11 @@ namespace Squidex.Infrastructure.States
var commitId = Guid.NewGuid(); var commitId = Guid.NewGuid();
foreach (var @event in eventArray)
{
eventEnricher.Enrich(@event, ownerKey);
}
var eventStream = GetStreamName(); var eventStream = GetStreamName();
var eventData = GetEventData(eventArray, commitId); var eventData = GetEventData(eventArray, commitId);

7
src/Squidex.Infrastructure/States/Store.cs

@ -16,15 +16,18 @@ namespace Squidex.Infrastructure.States
private readonly IServiceProvider services; private readonly IServiceProvider services;
private readonly IStreamNameResolver streamNameResolver; private readonly IStreamNameResolver streamNameResolver;
private readonly IEventStore eventStore; private readonly IEventStore eventStore;
private readonly IEventEnricher<TKey> eventEnricher;
private readonly IEventDataFormatter eventDataFormatter; private readonly IEventDataFormatter eventDataFormatter;
public Store( public Store(
IEventStore eventStore, IEventStore eventStore,
IEventEnricher<TKey> eventEnricher,
IEventDataFormatter eventDataFormatter, IEventDataFormatter eventDataFormatter,
IServiceProvider services, IServiceProvider services,
IStreamNameResolver streamNameResolver) IStreamNameResolver streamNameResolver)
{ {
this.eventStore = eventStore; this.eventStore = eventStore;
this.eventEnricher = eventEnricher;
this.eventDataFormatter = eventDataFormatter; this.eventDataFormatter = eventDataFormatter;
this.services = services; this.services = services;
this.streamNameResolver = streamNameResolver; this.streamNameResolver = streamNameResolver;
@ -51,7 +54,7 @@ namespace Squidex.Infrastructure.States
var snapshotStore = GetSnapshotStore<None>(); var snapshotStore = GetSnapshotStore<None>();
return new Persistence<TKey>(key, owner, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent); return new Persistence<TKey>(key, owner, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, applyEvent);
} }
private IPersistence<TState> CreatePersistence<TState>(Type owner, TKey key, PersistenceMode mode, HandleSnapshot<TState> applySnapshot, HandleEvent applyEvent) private IPersistence<TState> CreatePersistence<TState>(Type owner, TKey key, PersistenceMode mode, HandleSnapshot<TState> applySnapshot, HandleEvent applyEvent)
@ -60,7 +63,7 @@ namespace Squidex.Infrastructure.States
var snapshotStore = GetSnapshotStore<TState>(); var snapshotStore = GetSnapshotStore<TState>();
return new Persistence<TState, TKey>(key, owner, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent); return new Persistence<TState, TKey>(key, owner, eventStore, eventEnricher, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent);
} }
public Task ClearSnapshotsAsync<TState>() public Task ClearSnapshotsAsync<TState>()

2
src/Squidex/Config/Domain/EntitiesServices.cs

@ -283,6 +283,8 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<UsageTrackerCommandMiddleware>() services.AddSingletonAs<UsageTrackerCommandMiddleware>()
.As<ICommandMiddleware>(); .As<ICommandMiddleware>();
services.AddSingleton(typeof(IEventEnricher<>), typeof(SquidexEventEnricher<>));
} }
private static void AddBackupHandlers(this IServiceCollection services) private static void AddBackupHandlers(this IServiceCollection services)

1
src/Squidex/Config/Domain/SerializationServices.cs

@ -34,6 +34,7 @@ namespace Squidex.Config.Domain
new AppContributorsConverter(), new AppContributorsConverter(),
new AppPatternsConverter(), new AppPatternsConverter(),
new ClaimsPrincipalConverter(), new ClaimsPrincipalConverter(),
new ContentFieldDataConverter(),
new EnvelopeHeadersConverter(), new EnvelopeHeadersConverter(),
new FilterConverter(), new FilterConverter(),
new InstantConverter(), new InstantConverter(),

10
src/Squidex/Config/Orleans/OrleansServices.cs

@ -30,8 +30,10 @@ namespace Squidex.Config.Orleans
{ {
builder.ConfigureServices(siloServices => builder.ConfigureServices(siloServices =>
{ {
siloServices.AddSingleton<IIncomingGrainCallFilter, LocalCacheFilter>(); siloServices.AddSingleton<IActivationLimiter, ActivationLimiter>();
siloServices.AddSingleton<IIncomingGrainCallFilter, LoggingFilter>(); siloServices.AddScoped<IActivationLimit, ActivationLimit>();
siloServices.AddScoped(typeof(IGrainState<>), typeof(GrainState<>));
}); });
builder.ConfigureApplicationParts(parts => builder.ConfigureApplicationParts(parts =>
@ -60,6 +62,10 @@ namespace Squidex.Config.Orleans
options.HostSelf = false; options.HostSelf = false;
}); });
builder.AddIncomingGrainCallFilter<ActivationLimiterFilter>();
builder.AddIncomingGrainCallFilter<LocalCacheFilter>();
builder.AddIncomingGrainCallFilter<LoggingFilter>();
var orleansPortSilo = config.GetOptionalValue("orleans:siloPort", 11111); var orleansPortSilo = config.GetOptionalValue("orleans:siloPort", 11111);
var orleansPortGateway = config.GetOptionalValue("orleans:gatewayPort", 40000); var orleansPortGateway = config.GetOptionalValue("orleans:gatewayPort", 40000);

66
tests/Squidex.Domain.Apps.Core.Tests/Model/Contents/ContentFieldDataTests.cs

@ -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()));
}
}
}

1
tests/Squidex.Domain.Apps.Core.Tests/TestUtils.cs

@ -46,6 +46,7 @@ namespace Squidex.Domain.Apps.Core
new AppContributorsConverter(), new AppContributorsConverter(),
new AppPatternsConverter(), new AppPatternsConverter(),
new ClaimsPrincipalConverter(), new ClaimsPrincipalConverter(),
new ContentFieldDataConverter(),
new EnvelopeHeadersConverter(), new EnvelopeHeadersConverter(),
new FilterConverter(), new FilterConverter(),
new InstantConverter(), new InstantConverter(),

9
tests/Squidex.Domain.Apps.Entities.Tests/Apps/AppUISettingsGrainTests.cs

@ -10,23 +10,18 @@ using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Infrastructure.Json.Objects; using Squidex.Infrastructure.Json.Objects;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Apps namespace Squidex.Domain.Apps.Entities.Apps
{ {
public sealed class AppUISettingsGrainTests public sealed class AppUISettingsGrainTests
{ {
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>(); private readonly IGrainState<AppUISettingsGrain.GrainState> grainState = A.Fake<IGrainState<AppUISettingsGrain.GrainState>>();
private readonly IPersistence<AppUISettingsGrain.GrainState> persistence = A.Fake<IPersistence<AppUISettingsGrain.GrainState>>();
private readonly AppUISettingsGrain sut; private readonly AppUISettingsGrain sut;
public AppUISettingsGrainTests() public AppUISettingsGrainTests()
{ {
A.CallTo(() => store.WithSnapshots(typeof(AppUISettingsGrain), Guid.Empty, A<HandleSnapshot<AppUISettingsGrain.GrainState>>.Ignored)) sut = new AppUISettingsGrain(grainState);
.Returns(persistence);
sut = new AppUISettingsGrain(store);
sut.ActivateAsync(Guid.Empty).Wait(); sut.ActivateAsync(Guid.Empty).Wait();
} }

15
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByNameIndexGrainTests.cs

@ -11,25 +11,20 @@ using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Orleans; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.States;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Apps.Indexes namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{ {
public class AppsByNameIndexGrainTests public class AppsByNameIndexGrainTests
{ {
private readonly IStore<string> store = A.Fake<IStore<string>>(); private readonly IGrainState<AppsByNameIndexGrain.GrainState> grainState = A.Fake<IGrainState<AppsByNameIndexGrain.GrainState>>();
private readonly IPersistence<AppsByNameIndexGrain.GrainState> persistence = A.Fake<IPersistence<AppsByNameIndexGrain.GrainState>>();
private readonly NamedId<Guid> appId1 = NamedId.Of(Guid.NewGuid(), "my-app1"); private readonly NamedId<Guid> appId1 = NamedId.Of(Guid.NewGuid(), "my-app1");
private readonly NamedId<Guid> appId2 = NamedId.Of(Guid.NewGuid(), "my-app2"); private readonly NamedId<Guid> appId2 = NamedId.Of(Guid.NewGuid(), "my-app2");
private readonly AppsByNameIndexGrain sut; private readonly AppsByNameIndexGrain sut;
public AppsByNameIndexGrainTests() public AppsByNameIndexGrainTests()
{ {
A.CallTo(() => store.WithSnapshots(typeof(AppsByNameIndexGrain), SingleGrain.Id, A<HandleSnapshot<AppsByNameIndexGrain.GrainState>>.Ignored)) sut = new AppsByNameIndexGrain(grainState);
.Returns(persistence);
sut = new AppsByNameIndexGrain(store);
sut.ActivateAsync(SingleGrain.Id).Wait(); sut.ActivateAsync(SingleGrain.Id).Wait();
} }
@ -42,7 +37,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
Assert.Equal(appId1.Id, result); Assert.Equal(appId1.Id, result);
A.CallTo(() => persistence.WriteSnapshotAsync(A<AppsByNameIndexGrain.GrainState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(); .MustHaveHappened();
} }
@ -125,7 +120,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
Assert.Equal(Guid.Empty, result); Assert.Equal(Guid.Empty, result);
A.CallTo(() => persistence.WriteSnapshotAsync(A<AppsByNameIndexGrain.GrainState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappenedTwiceExactly(); .MustHaveHappenedTwiceExactly();
} }
@ -147,7 +142,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
Assert.Equal(2, await sut.CountAsync()); Assert.Equal(2, await sut.CountAsync());
A.CallTo(() => persistence.WriteSnapshotAsync(A<AppsByNameIndexGrain.GrainState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(); .MustHaveHappened();
} }
} }

18
tests/Squidex.Domain.Apps.Entities.Tests/Apps/Indexes/AppsByUserIndexGrainTests.cs

@ -1,4 +1,4 @@
// ========================================================================== // ==========================================================================.WriteAsync()
// Squidex Headless CMS // Squidex Headless CMS
// ========================================================================== // ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt) // Copyright (c) Squidex UG (haftungsbeschraenkt)
@ -10,15 +10,14 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.Orleans;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Apps.Indexes namespace Squidex.Domain.Apps.Entities.Apps.Indexes
{ {
public class AppsByUserIndexGrainTests public class AppsByUserIndexGrainTests
{ {
private readonly IStore<string> store = A.Fake<IStore<string>>(); private readonly IGrainState<AppsByUserIndexGrain.GrainState> grainState = A.Fake<IGrainState<AppsByUserIndexGrain.GrainState>>();
private readonly IPersistence<AppsByUserIndexGrain.GrainState> persistence = A.Fake<IPersistence<AppsByUserIndexGrain.GrainState>>();
private readonly Guid appId1 = Guid.NewGuid(); private readonly Guid appId1 = Guid.NewGuid();
private readonly Guid appId2 = Guid.NewGuid(); private readonly Guid appId2 = Guid.NewGuid();
private readonly string userId = "user"; private readonly string userId = "user";
@ -26,10 +25,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
public AppsByUserIndexGrainTests() public AppsByUserIndexGrainTests()
{ {
A.CallTo(() => store.WithSnapshots(typeof(AppsByUserIndexGrain), userId, A<HandleSnapshot<AppsByUserIndexGrain.GrainState>>.Ignored)) sut = new AppsByUserIndexGrain(grainState);
.Returns(persistence);
sut = new AppsByUserIndexGrain(store);
sut.ActivateAsync(userId).Wait(); sut.ActivateAsync(userId).Wait();
} }
@ -43,7 +39,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
Assert.Equal(new List<Guid> { appId1, appId2 }, result); Assert.Equal(new List<Guid> { appId1, appId2 }, result);
A.CallTo(() => persistence.WriteSnapshotAsync(A<AppsByUserIndexGrain.GrainState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappenedTwiceExactly(); .MustHaveHappenedTwiceExactly();
} }
@ -58,7 +54,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
Assert.Equal(new List<Guid> { appId2 }, result); Assert.Equal(new List<Guid> { appId2 }, result);
A.CallTo(() => persistence.WriteSnapshotAsync(A<AppsByUserIndexGrain.GrainState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappenedTwiceOrMore(); .MustHaveHappenedTwiceOrMore();
} }
@ -73,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Apps.Indexes
Assert.Equal(new List<Guid> { appId1, appId2 }, result); Assert.Equal(new List<Guid> { appId1, appId2 }, result);
A.CallTo(() => persistence.WriteSnapshotAsync(A<AppsByUserIndexGrain.GrainState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(); .MustHaveHappened();
} }
} }

3
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetCommandMiddlewareTests.cs

@ -21,6 +21,7 @@ using Squidex.Domain.Apps.Entities.TestHelpers;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.Reflection; using Squidex.Infrastructure.Reflection;
using Xunit; using Xunit;
@ -55,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
{ {
file = new AssetFile("my-image.png", "image/png", 1024, () => stream); file = new AssetFile("my-image.png", "image/png", 1024, () => stream);
asset = new AssetGrain(Store, tagService, A.Dummy<ISemanticLog>()); asset = new AssetGrain(Store, tagService, A.Fake<IActivationLimit>(), A.Dummy<ISemanticLog>());
asset.ActivateAsync(Id).Wait(); asset.ActivateAsync(Id).Wait();
A.CallTo(() => assetEnricher.EnrichAsync(A<IAssetEntity>.Ignored)) A.CallTo(() => assetEnricher.EnrichAsync(A<IAssetEntity>.Ignored))

11
tests/Squidex.Domain.Apps.Entities.Tests/Assets/AssetGrainTests.cs

@ -19,6 +19,7 @@ using Squidex.Infrastructure;
using Squidex.Infrastructure.Assets; using Squidex.Infrastructure.Assets;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Assets namespace Squidex.Domain.Apps.Entities.Assets
@ -26,6 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Assets
public class AssetGrainTests : HandlerTestBase<AssetState> public class AssetGrainTests : HandlerTestBase<AssetState>
{ {
private readonly ITagService tagService = A.Fake<ITagService>(); private readonly ITagService tagService = A.Fake<ITagService>();
private readonly IActivationLimit limit = A.Fake<IActivationLimit>();
private readonly ImageInfo image = new ImageInfo(2048, 2048); private readonly ImageInfo image = new ImageInfo(2048, 2048);
private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream()); private readonly AssetFile file = new AssetFile("my-image.png", "image/png", 1024, () => new MemoryStream());
private readonly Guid assetId = Guid.NewGuid(); private readonly Guid assetId = Guid.NewGuid();
@ -42,10 +44,17 @@ namespace Squidex.Domain.Apps.Entities.Assets
A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored)) A.CallTo(() => tagService.NormalizeTagsAsync(AppId, TagGroups.Assets, A<HashSet<string>>.Ignored, A<HashSet<string>>.Ignored))
.Returns(new Dictionary<string, string>()); .Returns(new Dictionary<string, string>());
sut = new AssetGrain(Store, tagService, A.Dummy<ISemanticLog>()); sut = new AssetGrain(Store, tagService, limit, A.Dummy<ISemanticLog>());
sut.ActivateAsync(Id).Wait(); sut.ActivateAsync(Id).Wait();
} }
[Fact]
public void Should_set_limit()
{
A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5)))
.MustHaveHappened();
}
[Fact] [Fact]
public async Task Command_should_throw_exception_if_rule_is_deleted() public async Task Command_should_throw_exception_if_rule_is_deleted()
{ {

11
tests/Squidex.Domain.Apps.Entities.Tests/Contents/ContentGrainTests.cs

@ -24,6 +24,7 @@ using Squidex.Domain.Apps.Events.Contents;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.Commands; using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Orleans;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Contents namespace Squidex.Domain.Apps.Entities.Contents
@ -31,6 +32,7 @@ namespace Squidex.Domain.Apps.Entities.Contents
public class ContentGrainTests : HandlerTestBase<ContentState> public class ContentGrainTests : HandlerTestBase<ContentState>
{ {
private readonly Guid contentId = Guid.NewGuid(); private readonly Guid contentId = Guid.NewGuid();
private readonly IActivationLimit limit = A.Fake<IActivationLimit>();
private readonly IAppEntity app; private readonly IAppEntity app;
private readonly IAppProvider appProvider = A.Fake<IAppProvider>(); private readonly IAppProvider appProvider = A.Fake<IAppProvider>();
private readonly IContentRepository contentRepository = A.Dummy<IContentRepository>(); private readonly IContentRepository contentRepository = A.Dummy<IContentRepository>();
@ -105,10 +107,17 @@ namespace Squidex.Domain.Apps.Entities.Contents
patched = patch.MergeInto(data); patched = patch.MergeInto(data);
sut = new ContentGrain(Store, A.Dummy<ISemanticLog>(), appProvider, A.Dummy<IAssetRepository>(), scriptEngine, contentWorkflow, contentRepository); sut = new ContentGrain(Store, A.Dummy<ISemanticLog>(), appProvider, A.Dummy<IAssetRepository>(), scriptEngine, contentWorkflow, contentRepository, limit);
sut.ActivateAsync(Id).Wait(); sut.ActivateAsync(Id).Wait();
} }
[Fact]
public void Should_set_limit()
{
A.CallTo(() => limit.SetLimit(5000, TimeSpan.FromMinutes(5)))
.MustHaveHappened();
}
[Fact] [Fact]
public async Task Command_should_throw_exception_if_content_is_deleted() public async Task Command_should_throw_exception_if_content_is_deleted()
{ {

19
tests/Squidex.Domain.Apps.Entities.Tests/Rules/Indexes/RulesByAppIndexGrainTests.cs

@ -9,15 +9,14 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.Orleans;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Rules.Indexes namespace Squidex.Domain.Apps.Entities.Rules.Indexes
{ {
public class RulesByAppIndexGrainTests public class RulesByAppIndexGrainTests
{ {
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>(); private readonly IGrainState<RulesByAppIndexGrain.GrainState> grainState = A.Fake<IGrainState<RulesByAppIndexGrain.GrainState>>();
private readonly IPersistence<RulesByAppIndexGrain.GrainState> persistence = A.Fake<IPersistence<RulesByAppIndexGrain.GrainState>>();
private readonly Guid appId = Guid.NewGuid(); private readonly Guid appId = Guid.NewGuid();
private readonly Guid ruleId1 = Guid.NewGuid(); private readonly Guid ruleId1 = Guid.NewGuid();
private readonly Guid ruleId2 = Guid.NewGuid(); private readonly Guid ruleId2 = Guid.NewGuid();
@ -25,10 +24,10 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes
public RulesByAppIndexGrainTests() public RulesByAppIndexGrainTests()
{ {
A.CallTo(() => store.WithSnapshots(typeof(RulesByAppIndexGrain), appId, A<HandleSnapshot<RulesByAppIndexGrain.GrainState>>.Ignored)) A.CallTo(() => grainState.ClearAsync())
.Returns(persistence); .Invokes(() => grainState.Value = new RulesByAppIndexGrain.GrainState());
sut = new RulesByAppIndexGrain(store); sut = new RulesByAppIndexGrain(grainState);
sut.ActivateAsync(appId).Wait(); sut.ActivateAsync(appId).Wait();
} }
@ -42,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes
Assert.Equal(new List<Guid> { ruleId1, ruleId2 }, result); Assert.Equal(new List<Guid> { ruleId1, ruleId2 }, result);
A.CallTo(() => persistence.WriteSnapshotAsync(A<RulesByAppIndexGrain.GrainState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappenedTwiceExactly(); .MustHaveHappenedTwiceExactly();
} }
@ -57,7 +56,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes
Assert.Empty(ids); Assert.Empty(ids);
A.CallTo(() => persistence.DeleteAsync()) A.CallTo(() => grainState.ClearAsync())
.MustHaveHappened(); .MustHaveHappened();
} }
@ -72,7 +71,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes
Assert.Equal(new List<Guid> { ruleId2 }, result); Assert.Equal(new List<Guid> { ruleId2 }, result);
A.CallTo(() => persistence.WriteSnapshotAsync(A<RulesByAppIndexGrain.GrainState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappenedTwiceOrMore(); .MustHaveHappenedTwiceOrMore();
} }
@ -91,7 +90,7 @@ namespace Squidex.Domain.Apps.Entities.Rules.Indexes
Assert.Equal(new List<Guid> { ruleId1, ruleId2 }, result); Assert.Equal(new List<Guid> { ruleId1, ruleId2 }, result);
A.CallTo(() => persistence.WriteSnapshotAsync(A<RulesByAppIndexGrain.GrainState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(); .MustHaveHappened();
} }
} }

19
tests/Squidex.Domain.Apps.Entities.Tests/Schemas/Indexes/SchemasByAppIndexGrainTests.cs

@ -10,15 +10,14 @@ using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.Orleans;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Schemas.Indexes namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
{ {
public class SchemasByAppIndexGrainTests public class SchemasByAppIndexGrainTests
{ {
private readonly IStore<Guid> store = A.Fake<IStore<Guid>>(); private readonly IGrainState<SchemasByAppIndexGrain.GrainState> grainState = A.Fake<IGrainState<SchemasByAppIndexGrain.GrainState>>();
private readonly IPersistence<SchemasByAppIndexGrain.GrainState> persistence = A.Fake<IPersistence<SchemasByAppIndexGrain.GrainState>>();
private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app"); private readonly NamedId<Guid> appId = NamedId.Of(Guid.NewGuid(), "my-app");
private readonly NamedId<Guid> schemaId1 = NamedId.Of(Guid.NewGuid(), "my-schema1"); private readonly NamedId<Guid> schemaId1 = NamedId.Of(Guid.NewGuid(), "my-schema1");
private readonly NamedId<Guid> schemaId2 = NamedId.Of(Guid.NewGuid(), "my-schema2"); private readonly NamedId<Guid> schemaId2 = NamedId.Of(Guid.NewGuid(), "my-schema2");
@ -26,10 +25,10 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
public SchemasByAppIndexGrainTests() public SchemasByAppIndexGrainTests()
{ {
A.CallTo(() => store.WithSnapshots(typeof(SchemasByAppIndexGrain), appId.Id, A<HandleSnapshot<SchemasByAppIndexGrain.GrainState>>.Ignored)) A.CallTo(() => grainState.ClearAsync())
.Returns(persistence); .Invokes(() => grainState.Value = new SchemasByAppIndexGrain.GrainState());
sut = new SchemasByAppIndexGrain(store); sut = new SchemasByAppIndexGrain(grainState);
sut.ActivateAsync(appId.Id).Wait(); sut.ActivateAsync(appId.Id).Wait();
} }
@ -42,7 +41,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
Assert.Equal(schemaId1.Id, result); Assert.Equal(schemaId1.Id, result);
A.CallTo(() => persistence.WriteSnapshotAsync(A<SchemasByAppIndexGrain.GrainState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(); .MustHaveHappened();
} }
@ -56,7 +55,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
Assert.Equal(id, Guid.Empty); Assert.Equal(id, Guid.Empty);
A.CallTo(() => persistence.DeleteAsync()) A.CallTo(() => grainState.ClearAsync())
.MustHaveHappened(); .MustHaveHappened();
} }
@ -70,7 +69,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
Assert.Equal(Guid.Empty, result); Assert.Equal(Guid.Empty, result);
A.CallTo(() => persistence.WriteSnapshotAsync(A<SchemasByAppIndexGrain.GrainState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappenedTwiceExactly(); .MustHaveHappenedTwiceExactly();
} }
@ -90,7 +89,7 @@ namespace Squidex.Domain.Apps.Entities.Schemas.Indexes
Assert.Equal(new List<Guid> { schemaId1.Id, schemaId2.Id }, await sut.GetSchemaIdsAsync()); Assert.Equal(new List<Guid> { schemaId1.Id, schemaId2.Id }, await sut.GetSchemaIdsAsync());
A.CallTo(() => persistence.WriteSnapshotAsync(A<SchemasByAppIndexGrain.GrainState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(); .MustHaveHappened();
} }
} }

13
tests/Squidex.Domain.Apps.Entities.Tests/Tags/TagGrainTests.cs

@ -11,24 +11,23 @@ using System.Threading.Tasks;
using FakeItEasy; using FakeItEasy;
using Squidex.Domain.Apps.Core.Tags; using Squidex.Domain.Apps.Core.Tags;
using Squidex.Infrastructure; using Squidex.Infrastructure;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.Orleans;
using Xunit; using Xunit;
namespace Squidex.Domain.Apps.Entities.Tags namespace Squidex.Domain.Apps.Entities.Tags
{ {
public class TagGrainTests public class TagGrainTests
{ {
private readonly IStore<string> store = A.Fake<IStore<string>>(); private readonly IGrainState<TagGrain.GrainState> grainState = A.Fake<IGrainState<TagGrain.GrainState>>();
private readonly IPersistence<TagGrain.GrainState> persistence = A.Fake<IPersistence<TagGrain.GrainState>>();
private readonly string id = Guid.NewGuid().ToString(); private readonly string id = Guid.NewGuid().ToString();
private readonly TagGrain sut; private readonly TagGrain sut;
public TagGrainTests() public TagGrainTests()
{ {
A.CallTo(() => store.WithSnapshots(typeof(TagGrain), id, A<HandleSnapshot<TagGrain.GrainState>>.Ignored)) A.CallTo(() => grainState.ClearAsync())
.Returns(persistence); .Invokes(() => grainState.Value = new TagGrain.GrainState());
sut = new TagGrain(store); sut = new TagGrain(grainState);
sut.ActivateAsync(id).Wait(); sut.ActivateAsync(id).Wait();
} }
@ -43,7 +42,7 @@ namespace Squidex.Domain.Apps.Entities.Tags
Assert.Empty(allTags); Assert.Empty(allTags);
A.CallTo(() => persistence.DeleteAsync()) A.CallTo(() => grainState.ClearAsync())
.MustHaveHappened(); .MustHaveHappened();
} }

18
tests/Squidex.Infrastructure.Tests/Caching/LRUCacheTests.cs

@ -5,6 +5,7 @@
// All rights reserved. Licensed under the MIT license. // All rights reserved. Licensed under the MIT license.
// ========================================================================== // ==========================================================================
using System.Collections.Generic;
using Xunit; using Xunit;
namespace Squidex.Infrastructure.Caching namespace Squidex.Infrastructure.Caching
@ -47,6 +48,23 @@ namespace Squidex.Infrastructure.Caching
} }
} }
[Fact]
public void Should_notify_about_evicted_items()
{
var evicted = new List<int>();
var cache = new LRUCache<int, int>(3, (key, _) => evicted.Add(key));
cache.Set(1, 1);
cache.Set(2, 2);
cache.Set(3, 3);
cache.Set(1, 1);
cache.Set(4, 4);
cache.Set(5, 5);
Assert.Equal(new List<int> { 2, 3 }, evicted);
}
[Fact] [Fact]
public void Should_return_false_when_item_to_remove_does_not_exist() public void Should_return_false_when_item_to_remove_does_not_exist()
{ {

84
tests/Squidex.Infrastructure.Tests/EventSourcing/Grains/EventConsumerGrainTests.cs

@ -11,7 +11,7 @@ using FakeItEasy;
using FluentAssertions; using FluentAssertions;
using Orleans.Concurrency; using Orleans.Concurrency;
using Squidex.Infrastructure.Log; using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.States; using Squidex.Infrastructure.Orleans;
using Squidex.Infrastructure.TestHelpers; using Squidex.Infrastructure.TestHelpers;
using Xunit; using Xunit;
@ -23,11 +23,11 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
{ {
public MyEventConsumerGrain( public MyEventConsumerGrain(
EventConsumerFactory eventConsumerFactory, EventConsumerFactory eventConsumerFactory,
IStore<string> store, IGrainState<EventConsumerState> state,
IEventStore eventStore, IEventStore eventStore,
IEventDataFormatter eventDataFormatter, IEventDataFormatter eventDataFormatter,
ISemanticLog log) ISemanticLog log)
: base(eventConsumerFactory, store, eventStore, eventDataFormatter, log) : base(eventConsumerFactory, state, eventStore, eventDataFormatter, log)
{ {
} }
@ -42,34 +42,24 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
} }
} }
private readonly IGrainState<EventConsumerState> grainState = A.Fake<IGrainState<EventConsumerState>>();
private readonly IEventConsumer eventConsumer = A.Fake<IEventConsumer>(); private readonly IEventConsumer eventConsumer = A.Fake<IEventConsumer>();
private readonly IEventStore eventStore = A.Fake<IEventStore>(); private readonly IEventStore eventStore = A.Fake<IEventStore>();
private readonly IEventSubscription eventSubscription = A.Fake<IEventSubscription>(); private readonly IEventSubscription eventSubscription = A.Fake<IEventSubscription>();
private readonly IPersistence<EventConsumerState> persistence = A.Fake<IPersistence<EventConsumerState>>();
private readonly ISemanticLog log = A.Fake<ISemanticLog>(); private readonly ISemanticLog log = A.Fake<ISemanticLog>();
private readonly IStore<string> store = A.Fake<IStore<string>>();
private readonly IEventDataFormatter formatter = A.Fake<IEventDataFormatter>(); private readonly IEventDataFormatter formatter = A.Fake<IEventDataFormatter>();
private readonly EventData eventData = new EventData("Type", new EnvelopeHeaders(), "Payload"); private readonly EventData eventData = new EventData("Type", new EnvelopeHeaders(), "Payload");
private readonly Envelope<IEvent> envelope = new Envelope<IEvent>(new MyEvent()); private readonly Envelope<IEvent> envelope = new Envelope<IEvent>(new MyEvent());
private readonly EventConsumerGrain sut; private readonly EventConsumerGrain sut;
private readonly string consumerName; private readonly string consumerName;
private readonly string initialPosition = Guid.NewGuid().ToString(); private readonly string initialPosition = Guid.NewGuid().ToString();
private HandleSnapshot<EventConsumerState> apply;
private EventConsumerState state = new EventConsumerState();
public EventConsumerGrainTests() public EventConsumerGrainTests()
{ {
state.Position = initialPosition; grainState.Value.Position = initialPosition;
consumerName = eventConsumer.GetType().Name; consumerName = eventConsumer.GetType().Name;
A.CallTo(() => store.WithSnapshots(A<Type>.Ignored, consumerName, A<HandleSnapshot<EventConsumerState>>.Ignored))
.Invokes(new Action<Type, string, HandleSnapshot<EventConsumerState>>((t, key, a) =>
{
apply = a;
}))
.Returns(persistence);
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)) A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored))
.Returns(eventSubscription); .Returns(eventSubscription);
@ -79,18 +69,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
A.CallTo(() => eventConsumer.Handles(A<StoredEvent>.Ignored)) A.CallTo(() => eventConsumer.Handles(A<StoredEvent>.Ignored))
.Returns(true); .Returns(true);
A.CallTo(() => persistence.ReadAsync(EtagVersion.Any))
.Invokes(new Action<long>(s => apply(state)));
A.CallTo(() => persistence.WriteSnapshotAsync(A<EventConsumerState>.Ignored))
.Invokes(new Action<EventConsumerState>(s => state = s));
A.CallTo(() => formatter.Parse(eventData, null)) A.CallTo(() => formatter.Parse(eventData, null))
.Returns(envelope); .Returns(envelope);
sut = new MyEventConsumerGrain( sut = new MyEventConsumerGrain(
x => eventConsumer, x => eventConsumer,
store, grainState,
eventStore, eventStore,
formatter, formatter,
log); log);
@ -99,12 +83,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
[Fact] [Fact]
public async Task Should_not_subscribe_to_event_store_when_stopped_in_db() public async Task Should_not_subscribe_to_event_store_when_stopped_in_db()
{ {
state = state.Stopped(); grainState.Value = grainState.Value.Stopped();
await sut.ActivateAsync(consumerName); await sut.ActivateAsync(consumerName);
await sut.ActivateAsync(); await sut.ActivateAsync();
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null });
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)) A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored))
.MustNotHaveHappened(); .MustNotHaveHappened();
@ -116,7 +100,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await sut.ActivateAsync(consumerName); await sut.ActivateAsync(consumerName);
await sut.ActivateAsync(); await sut.ActivateAsync();
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null });
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)) A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored))
.MustHaveHappened(1, Times.Exactly); .MustHaveHappened(1, Times.Exactly);
@ -128,7 +112,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await sut.ActivateAsync(consumerName); await sut.ActivateAsync(consumerName);
await sut.ActivateAsync(); await sut.ActivateAsync();
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null });
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored)) A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, A<string>.Ignored))
.MustHaveHappened(1, Times.Exactly); .MustHaveHappened(1, Times.Exactly);
@ -142,9 +126,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await sut.StopAsync(); await sut.StopAsync();
await sut.StopAsync(); await sut.StopAsync();
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = null });
A.CallTo(() => persistence.WriteSnapshotAsync(A<EventConsumerState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(1, Times.Exactly); .MustHaveHappened(1, Times.Exactly);
A.CallTo(() => eventSubscription.StopAsync()) A.CallTo(() => eventSubscription.StopAsync())
@ -159,9 +143,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await sut.StopAsync(); await sut.StopAsync();
await sut.ResetAsync(); await sut.ResetAsync();
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = null, Error = null }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = null, Error = null });
A.CallTo(() => persistence.WriteSnapshotAsync(A<EventConsumerState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(2, Times.Exactly); .MustHaveHappened(2, Times.Exactly);
A.CallTo(() => eventConsumer.ClearAsync()) A.CallTo(() => eventConsumer.ClearAsync())
@ -170,7 +154,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
A.CallTo(() => eventSubscription.StopAsync()) A.CallTo(() => eventSubscription.StopAsync())
.MustHaveHappened(1, Times.Exactly); .MustHaveHappened(1, Times.Exactly);
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, state.Position)) A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, grainState.Value.Position))
.MustHaveHappened(1, Times.Exactly); .MustHaveHappened(1, Times.Exactly);
A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, null)) A.CallTo(() => eventStore.CreateSubscription(A<IEventSubscriber>.Ignored, A<string>.Ignored, null))
@ -187,9 +171,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await OnEventAsync(eventSubscription, @event); await OnEventAsync(eventSubscription, @event);
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null });
A.CallTo(() => persistence.WriteSnapshotAsync(A<EventConsumerState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(1, Times.Exactly); .MustHaveHappened(1, Times.Exactly);
A.CallTo(() => eventConsumer.On(envelope)) A.CallTo(() => eventConsumer.On(envelope))
@ -209,9 +193,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await OnEventAsync(eventSubscription, @event); await OnEventAsync(eventSubscription, @event);
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null });
A.CallTo(() => persistence.WriteSnapshotAsync(A<EventConsumerState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(1, Times.Exactly); .MustHaveHappened(1, Times.Exactly);
A.CallTo(() => eventConsumer.On(envelope)) A.CallTo(() => eventConsumer.On(envelope))
@ -231,9 +215,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await OnEventAsync(eventSubscription, @event); await OnEventAsync(eventSubscription, @event);
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = @event.EventPosition, Error = null });
A.CallTo(() => persistence.WriteSnapshotAsync(A<EventConsumerState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(1, Times.Exactly); .MustHaveHappened(1, Times.Exactly);
A.CallTo(() => eventConsumer.On(envelope)) A.CallTo(() => eventConsumer.On(envelope))
@ -250,7 +234,7 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await OnEventAsync(A.Fake<IEventSubscription>(), @event); await OnEventAsync(A.Fake<IEventSubscription>(), @event);
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null });
A.CallTo(() => eventConsumer.On(envelope)) A.CallTo(() => eventConsumer.On(envelope))
.MustNotHaveHappened(); .MustNotHaveHappened();
@ -266,9 +250,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await OnErrorAsync(eventSubscription, ex); await OnErrorAsync(eventSubscription, ex);
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() });
A.CallTo(() => persistence.WriteSnapshotAsync(A<EventConsumerState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(1, Times.Exactly); .MustHaveHappened(1, Times.Exactly);
A.CallTo(() => eventSubscription.StopAsync()) A.CallTo(() => eventSubscription.StopAsync())
@ -285,9 +269,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await OnErrorAsync(A.Fake<IEventSubscription>(), ex); await OnErrorAsync(A.Fake<IEventSubscription>(), ex);
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null });
A.CallTo(() => persistence.WriteSnapshotAsync(A<EventConsumerState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustNotHaveHappened(); .MustNotHaveHappened();
} }
@ -314,9 +298,9 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await sut.ActivateAsync(); await sut.ActivateAsync();
await sut.ResetAsync(); await sut.ResetAsync();
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() });
A.CallTo(() => persistence.WriteSnapshotAsync(A<EventConsumerState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(1, Times.Exactly); .MustHaveHappened(1, Times.Exactly);
A.CallTo(() => eventSubscription.StopAsync()) A.CallTo(() => eventSubscription.StopAsync())
@ -338,12 +322,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await OnEventAsync(eventSubscription, @event); await OnEventAsync(eventSubscription, @event);
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() });
A.CallTo(() => eventConsumer.On(envelope)) A.CallTo(() => eventConsumer.On(envelope))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => persistence.WriteSnapshotAsync(A<EventConsumerState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(1, Times.Exactly); .MustHaveHappened(1, Times.Exactly);
A.CallTo(() => eventSubscription.StopAsync()) A.CallTo(() => eventSubscription.StopAsync())
@ -365,12 +349,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await OnEventAsync(eventSubscription, @event); await OnEventAsync(eventSubscription, @event);
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = true, Position = initialPosition, Error = ex.ToString() });
A.CallTo(() => eventConsumer.On(envelope)) A.CallTo(() => eventConsumer.On(envelope))
.MustNotHaveHappened(); .MustNotHaveHappened();
A.CallTo(() => persistence.WriteSnapshotAsync(A<EventConsumerState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(1, Times.Exactly); .MustHaveHappened(1, Times.Exactly);
A.CallTo(() => eventSubscription.StopAsync()) A.CallTo(() => eventSubscription.StopAsync())
@ -396,12 +380,12 @@ namespace Squidex.Infrastructure.EventSourcing.Grains
await sut.StartAsync(); await sut.StartAsync();
await sut.StartAsync(); await sut.StartAsync();
state.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null }); grainState.Value.Should().BeEquivalentTo(new EventConsumerState { IsStopped = false, Position = initialPosition, Error = null });
A.CallTo(() => eventConsumer.On(envelope)) A.CallTo(() => eventConsumer.On(envelope))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => persistence.WriteSnapshotAsync(A<EventConsumerState>.Ignored)) A.CallTo(() => grainState.WriteAsync())
.MustHaveHappened(2, Times.Exactly); .MustHaveHappened(2, Times.Exactly);
A.CallTo(() => eventSubscription.StopAsync()) A.CallTo(() => eventSubscription.StopAsync())

35
tests/Squidex.Infrastructure.Tests/MongoDb/MongoExtensionsTests.cs

@ -36,9 +36,12 @@ namespace Squidex.Infrastructure.MongoDb
} }
} }
public Cursor<T> Add(T item) public Cursor<T> Add(params T[] newItems)
{
foreach (var item in newItems)
{ {
items.Add(item); items.Add(item);
}
return this; return this;
} }
@ -74,7 +77,7 @@ namespace Squidex.Infrastructure.MongoDb
{ {
var result = new List<int>(); var result = new List<int>();
var cursor = new Cursor<int>().Add(0).Add(1).Add(1).Add(2).Add(3).Add(5); var cursor = new Cursor<int>().Add(0, 1, 2, 3, 4, 5);
await cursor.ForEachPipelineAsync(x => await cursor.ForEachPipelineAsync(x =>
{ {
@ -82,7 +85,7 @@ namespace Squidex.Infrastructure.MongoDb
return TaskHelper.Done; return TaskHelper.Done;
}); });
Assert.Equal(new List<int> { 0, 1, 1, 2, 3, 5 }, result); Assert.Equal(new List<int> { 0, 1, 2, 3, 4, 5 }, result);
} }
[Fact] [Fact]
@ -92,8 +95,8 @@ namespace Squidex.Infrastructure.MongoDb
var result = new List<int>(); var result = new List<int>();
var cursor = new Cursor<int>().Add(0).Add(1).Add(1).Add(ex).Add(2).Add(3).Add(5); using (var cursor = new Cursor<int>().Add(0, 1, 2).Add(ex).Add(3, 4, 5))
{
await Assert.ThrowsAsync<InvalidOperationException>(() => await Assert.ThrowsAsync<InvalidOperationException>(() =>
{ {
return cursor.ForEachPipelineAsync(x => return cursor.ForEachPipelineAsync(x =>
@ -102,8 +105,9 @@ namespace Squidex.Infrastructure.MongoDb
return TaskHelper.Done; return TaskHelper.Done;
}); });
}); });
}
Assert.Equal(new List<int> { 0, 1, 1 }, result); Assert.Equal(new List<int> { 0, 1, 2 }, result);
} }
[Fact] [Fact]
@ -113,8 +117,8 @@ namespace Squidex.Infrastructure.MongoDb
var result = new List<int>(); var result = new List<int>();
var cursor = new Cursor<int>().Add(0).Add(1).Add(1).Add(2).Add(3).Add(5); using (var cursor = new Cursor<int>().Add(0, 1, 2, 3, 4, 5))
{
await Assert.ThrowsAsync<InvalidOperationException>(() => await Assert.ThrowsAsync<InvalidOperationException>(() =>
{ {
return cursor.ForEachPipelineAsync(x => return cursor.ForEachPipelineAsync(x =>
@ -128,19 +132,20 @@ namespace Squidex.Infrastructure.MongoDb
return TaskHelper.Done; return TaskHelper.Done;
}); });
}); });
}
Assert.Equal(new List<int> { 0, 1, 1 }, result); Assert.Equal(new List<int> { 0, 1 }, result);
} }
[Fact] [Fact]
public async Task Should_stop_when_cancelled1() public async Task Should_stop_when_cancelled1()
{ {
var cts = new CancellationTokenSource(); using (var cts = new CancellationTokenSource())
{
var result = new List<int>(); var result = new List<int>();
var cursor = new Cursor<int>().Add(0).Add(1).Add(1).Add(2).Add(3).Add(5); using (var cursor = new Cursor<int>().Add(0, 1, 2, 3, 4, 5))
{
await Assert.ThrowsAnyAsync<OperationCanceledException>(() => await Assert.ThrowsAnyAsync<OperationCanceledException>(() =>
{ {
return cursor.ForEachPipelineAsync(x => return cursor.ForEachPipelineAsync(x =>
@ -155,8 +160,10 @@ namespace Squidex.Infrastructure.MongoDb
return TaskHelper.Done; return TaskHelper.Done;
}, cts.Token); }, cts.Token);
}); });
}
Assert.Equal(new List<int> { 0, 1, 1, 2 }, result); Assert.Equal(new List<int> { 0, 1, 2 }, result);
}
} }
} }
} }

83
tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterFilterTests.cs

@ -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();
}
}
}

98
tests/Squidex.Infrastructure.Tests/Orleans/ActivationLimiterTests.cs

@ -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;
}
}
}

101
tests/Squidex.Infrastructure.Tests/Orleans/GrainOfGuidTests.cs

@ -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();
}
}
}

101
tests/Squidex.Infrastructure.Tests/Orleans/GrainOfStringTests.cs

@ -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();
}
}
}

5
tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs

@ -19,6 +19,7 @@ namespace Squidex.Infrastructure.States
public class PersistenceEventSourcingTests public class PersistenceEventSourcingTests
{ {
private readonly string key = Guid.NewGuid().ToString(); private readonly string key = Guid.NewGuid().ToString();
private readonly IEventEnricher<string> eventEnricher = A.Fake<IEventEnricher<string>>();
private readonly IEventDataFormatter eventDataFormatter = A.Fake<IEventDataFormatter>(); private readonly IEventDataFormatter eventDataFormatter = A.Fake<IEventDataFormatter>();
private readonly IEventStore eventStore = A.Fake<IEventStore>(); private readonly IEventStore eventStore = A.Fake<IEventStore>();
private readonly IServiceProvider services = A.Fake<IServiceProvider>(); private readonly IServiceProvider services = A.Fake<IServiceProvider>();
@ -37,7 +38,7 @@ namespace Squidex.Infrastructure.States
A.CallTo(() => streamNameResolver.GetStreamName(None.Type, key)) A.CallTo(() => streamNameResolver.GetStreamName(None.Type, key))
.Returns(key); .Returns(key);
sut = new Store<string>(eventStore, eventDataFormatter, services, streamNameResolver); sut = new Store<string>(eventStore, eventEnricher, eventDataFormatter, services, streamNameResolver);
} }
[Fact] [Fact]
@ -190,6 +191,8 @@ namespace Squidex.Infrastructure.States
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, key, 3, A<ICollection<EventData>>.That.Matches(x => x.Count == 1))) A.CallTo(() => eventStore.AppendAsync(A<Guid>.Ignored, key, 3, A<ICollection<EventData>>.That.Matches(x => x.Count == 1)))
.MustHaveHappened(); .MustHaveHappened();
A.CallTo(() => eventEnricher.Enrich(A<Envelope<IEvent>>.Ignored, key))
.MustHaveHappenedTwiceExactly();
} }
[Fact] [Fact]

2
tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs

@ -30,7 +30,7 @@ namespace Squidex.Infrastructure.States
A.CallTo(() => services.GetService(typeof(ISnapshotStore<int, string>))) A.CallTo(() => services.GetService(typeof(ISnapshotStore<int, string>)))
.Returns(snapshotStore); .Returns(snapshotStore);
sut = new Store<string>(eventStore, eventDataFormatter, services, streamNameResolver); sut = new Store<string>(eventStore, new DefaultEventEnricher<string>(), eventDataFormatter, services, streamNameResolver);
} }
[Fact] [Fact]

75
tools/LoadTest/ClientQueryFixture.cs

@ -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()
{
}
}
}

95
tools/LoadTest/Model/TestClient.cs

@ -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;
}
}
}
}
}

2
tools/LoadTest/TestEntity.cs → tools/LoadTest/Model/TestEntity.cs

@ -8,7 +8,7 @@
using Newtonsoft.Json; using Newtonsoft.Json;
using Squidex.ClientLibrary; using Squidex.ClientLibrary;
namespace LoadTest namespace LoadTest.Model
{ {
public sealed class TestEntity : SquidexEntityBase<TestEntityData> public sealed class TestEntity : SquidexEntityBase<TestEntityData>
{ {

30
tools/LoadTest/ContentQueryBenchmarks.cs → tools/LoadTest/ReadingBenchmarks.cs

@ -12,19 +12,35 @@ using Xunit;
namespace LoadTest namespace LoadTest
{ {
public class ContentQueryBenchmarks : IClassFixture<ClientQueryFixture> public class ReadingBenchmarks : IClassFixture<ReadingFixture>
{ {
public ClientQueryFixture Fixture { get; } public ReadingFixture Fixture { get; }
public ContentQueryBenchmarks(ClientQueryFixture fixture) public ReadingBenchmarks(ReadingFixture fixture)
{ {
Fixture = fixture; Fixture = fixture;
} }
public static IEnumerable<object[]> Loads() public static IEnumerable<object[]> Loads()
{ {
int[] users = { 1, 5, 10, 20, 50, 100 }; int[] users =
int[] loads = { 5, 10, 20, 50, 100 }; {
1,
5,
10,
20,
50,
100
};
int[] loads =
{
5,
10,
20,
50,
100
};
foreach (var user in users) foreach (var user in users)
{ {
@ -33,6 +49,8 @@ namespace LoadTest
yield return new object[] { user, load }; yield return new object[] { user, load };
} }
} }
yield return new object[] { 1, 20000 };
} }
[Theory] [Theory]

46
tools/LoadTest/ReadingFixture.cs

@ -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()
{
}
}
}

34
tools/LoadTest/TestClient.cs

@ -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);
}
}
}

73
tools/LoadTest/Utils/Run.cs

@ -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);
}
}
}

30
tools/LoadTest/ContentCreationBenchmarks.cs → tools/LoadTest/WritingBenchmarks.cs

@ -8,23 +8,41 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using LoadTest.Model;
using LoadTest.Utils;
using Xunit; using Xunit;
namespace LoadTest namespace LoadTest
{ {
public class ContentCreationBenchmarks : IClassFixture<ClientQueryFixture> public class WritingBenchmarks : IClassFixture<WritingFixture>
{ {
public ClientQueryFixture Fixture { get; } public WritingFixture Fixture { get; }
public ContentCreationBenchmarks(ClientQueryFixture fixture) public WritingBenchmarks(WritingFixture fixture)
{ {
Fixture = fixture; Fixture = fixture;
} }
public static IEnumerable<object[]> Loads() public static IEnumerable<object[]> Loads()
{ {
int[] users = { 1, 5, 10, 20, 50, 100 }; int[] users =
int[] loads = { 5, 10, 20, 50, 100 }; {
1,
5,
10,
20,
50,
100
};
int[] loads =
{
5,
10,
20,
50,
100
};
foreach (var user in users) foreach (var user in users)
{ {
@ -33,6 +51,8 @@ namespace LoadTest
yield return new object[] { user, load }; yield return new object[] { user, load };
} }
} }
yield return new object[] { 1, 50000 };
} }
[Theory] [Theory]

31
tools/LoadTest/WritingFixture.cs

@ -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…
Cancel
Save