Browse Source

Performance improvements.

pull/285/head
Sebastian 8 years ago
parent
commit
1feae0de54
  1. 2
      src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs
  2. 22
      src/Squidex.Domain.Apps.Entities/AppProvider.cs
  3. 2
      src/Squidex.Domain.Users.MongoDb/MongoXmlRepository.cs
  4. 91
      src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs
  5. 68
      src/Squidex.Infrastructure/Caching/HttpRequestCache.cs
  6. 6
      src/Squidex.Infrastructure/Caching/ILocalCache.cs
  7. 4
      src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs
  8. 7
      src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs
  9. 2
      src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs
  10. 33
      src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs
  11. 1
      src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  12. 2
      src/Squidex.Infrastructure/States/IStore.cs
  13. 7
      src/Squidex.Infrastructure/States/Store.cs
  14. 3
      src/Squidex/Config/Domain/EntitiesServices.cs
  15. 4
      src/Squidex/Config/Domain/InfrastructureServices.cs
  16. 4
      src/Squidex/Config/Domain/StoreServices.cs
  17. 1
      src/Squidex/Config/Orleans/SiloWrapper.cs
  18. 7
      src/Squidex/Config/Web/WebExtensions.cs
  19. 9
      src/Squidex/Config/Web/WebServices.cs
  20. 8
      src/Squidex/Pipeline/EnforceHttpsMiddleware.cs
  21. 35
      src/Squidex/Pipeline/LocalCacheMiddleware.cs
  22. 10
      src/Squidex/Pipeline/RequestLogPerformanceMiddleware.cs
  23. 1
      src/Squidex/WebStartup.cs
  24. 2
      src/Squidex/app/features/content/pages/content/content-page.component.html
  25. 127
      tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs
  26. 133
      tests/Squidex.Infrastructure.Tests/Caching/HttpRequestCacheTests.cs
  27. 4
      tests/Squidex.Infrastructure.Tests/States/PersistenceEventSourcingTests.cs
  28. 13
      tests/Squidex.Infrastructure.Tests/States/PersistenceSnapshotTests.cs
  29. 40
      tools/Migrate_01/MigrationPath.cs
  30. 2
      tools/Migrate_01/Migrations/AddPatterns.cs
  31. 32
      tools/Migrate_01/Migrations/DeleteArchiveCollection.cs
  32. 11
      tools/Migrate_01/Migrations/DeleteContentCollections.cs
  33. 4
      tools/Migrate_01/Migrations/RebuildSnapshots.cs
  34. 154
      tools/Migrate_01/Rebuilder.cs

2
src/Squidex.Domain.Apps.Entities.MongoDb/Assets/MongoAssetRepository.cs

@ -84,7 +84,7 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.Assets
public async Task<IResultList<IAssetEntity>> QueryAsync(Guid appId, HashSet<Guid> ids)
{
var find = Collection.Find(Filter.In(x => x.Id, ids)).SortByDescending(x => x.LastModified);
var find = Collection.Find(x => ids.Contains(x.Id)).SortByDescending(x => x.LastModified);
var assetItems = find.ToListAsync();
var assetCount = find.CountAsync();

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

@ -23,20 +23,20 @@ namespace Squidex.Domain.Apps.Entities
public sealed class AppProvider : IAppProvider
{
private readonly IGrainFactory grainFactory;
private readonly IRequestCache requestCache;
private readonly ILocalCache localCache;
public AppProvider(IGrainFactory grainFactory, IRequestCache requestCache)
public AppProvider(IGrainFactory grainFactory, ILocalCache localCache)
{
Guard.NotNull(grainFactory, nameof(grainFactory));
Guard.NotNull(requestCache, nameof(requestCache));
Guard.NotNull(localCache, nameof(localCache));
this.grainFactory = grainFactory;
this.requestCache = requestCache;
this.localCache = localCache;
}
public Task<(IAppEntity, ISchemaEntity)> GetAppWithSchemaAsync(Guid appId, Guid id)
{
return requestCache.GetOrCreateAsync($"GetAppWithSchemaAsync({appId}, {id})", async () =>
return localCache.GetOrCreateAsync($"GetAppWithSchemaAsync({appId}, {id})", async () =>
{
using (Profile.Method<AppProvider>())
{
@ -61,7 +61,7 @@ namespace Squidex.Domain.Apps.Entities
public Task<IAppEntity> GetAppAsync(string appName)
{
return requestCache.GetOrCreateAsync($"GetAppAsync({appName})", async () =>
return localCache.GetOrCreateAsync($"GetAppAsync({appName})", async () =>
{
using (Profile.Method<AppProvider>())
{
@ -86,7 +86,7 @@ namespace Squidex.Domain.Apps.Entities
public Task<ISchemaEntity> GetSchemaAsync(Guid appId, string name)
{
return requestCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {name})", async () =>
return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {name})", async () =>
{
using (Profile.Method<AppProvider>())
{
@ -104,7 +104,7 @@ namespace Squidex.Domain.Apps.Entities
public Task<ISchemaEntity> GetSchemaAsync(Guid appId, Guid id, bool allowDeleted = false)
{
return requestCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {id}, {allowDeleted})", async () =>
return localCache.GetOrCreateAsync($"GetSchemaAsync({appId}, {id}, {allowDeleted})", async () =>
{
using (Profile.Method<AppProvider>())
{
@ -122,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities
public Task<List<ISchemaEntity>> GetSchemasAsync(Guid appId)
{
return requestCache.GetOrCreateAsync($"GetSchemasAsync({appId})", async () =>
return localCache.GetOrCreateAsync($"GetSchemasAsync({appId})", async () =>
{
using (Profile.Method<AppProvider>())
{
@ -139,7 +139,7 @@ namespace Squidex.Domain.Apps.Entities
public Task<List<IRuleEntity>> GetRulesAsync(Guid appId)
{
return requestCache.GetOrCreateAsync($"GetRulesAsync({appId})", async () =>
return localCache.GetOrCreateAsync($"GetRulesAsync({appId})", async () =>
{
using (Profile.Method<AppProvider>())
{
@ -156,7 +156,7 @@ namespace Squidex.Domain.Apps.Entities
public Task<List<IAppEntity>> GetUserApps(string userId)
{
return requestCache.GetOrCreateAsync($"GetUserApps({userId})", async () =>
return localCache.GetOrCreateAsync($"GetUserApps({userId})", async () =>
{
using (Profile.Method<AppProvider>())
{

2
src/Squidex.Domain.Users.MongoDb/MongoXmlRepository.cs

@ -36,7 +36,7 @@ namespace Squidex.Domain.Users.MongoDb
public void StoreElement(XElement element, string friendlyName)
{
Collection.UpdateOne(Filter.Eq(x => x.Id, friendlyName),
Collection.UpdateOne(x => x.Id == friendlyName,
Update.Set(x => x.Xml, element.ToString()),
Upsert);
}

91
src/Squidex.Infrastructure/Caching/AsyncLocalCache.cs

@ -0,0 +1,91 @@
// ==========================================================================
// 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;
namespace Squidex.Infrastructure.Caching
{
public sealed class AsyncLocalCache : ILocalCache
{
private static readonly AsyncLocal<ConcurrentDictionary<object, object>> Cache = new AsyncLocal<ConcurrentDictionary<object, object>>();
private static readonly AsyncLocalCleaner Cleaner;
private sealed class AsyncLocalCleaner : IDisposable
{
private readonly AsyncLocal<ConcurrentDictionary<object, object>> cache;
public AsyncLocalCleaner(AsyncLocal<ConcurrentDictionary<object, object>> cache)
{
this.cache = cache;
}
public void Dispose()
{
cache.Value = null;
}
}
static AsyncLocalCache()
{
Cleaner = new AsyncLocalCleaner(Cache);
}
public IDisposable StartContext()
{
Cache.Value = new ConcurrentDictionary<object, object>();
return Cleaner;
}
public void Add(object key, object value)
{
var cacheKey = GetCacheKey(key);
var cache = Cache.Value;
if (cache != null)
{
cache[cacheKey] = value;
}
}
public void Remove(object key)
{
var cacheKey = GetCacheKey(key);
var cache = Cache.Value;
if (cache != null)
{
cache.TryRemove(cacheKey, out var value);
}
}
public bool TryGetValue(object key, out object value)
{
var cacheKey = GetCacheKey(key);
var cache = Cache.Value;
if (cache != null)
{
return cache.TryGetValue(cacheKey, out value);
}
value = null;
return false;
}
private static string GetCacheKey(object key)
{
return $"CACHE_{key}";
}
}
}

68
src/Squidex.Infrastructure/Caching/HttpRequestCache.cs

@ -1,68 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using Microsoft.AspNetCore.Http;
namespace Squidex.Infrastructure.Caching
{
public sealed class HttpRequestCache : IRequestCache
{
private readonly IHttpContextAccessor httpContextAccessor;
public HttpRequestCache(IHttpContextAccessor httpContextAccessor)
{
Guard.NotNull(httpContextAccessor, nameof(httpContextAccessor));
this.httpContextAccessor = httpContextAccessor;
}
public void Add(object key, object value)
{
var cacheKey = GetCacheKey(key);
var items = httpContextAccessor.HttpContext?.Items;
if (items != null)
{
items[cacheKey] = value;
}
}
public void Remove(object key)
{
var cacheKey = GetCacheKey(key);
var items = httpContextAccessor.HttpContext?.Items;
if (items != null)
{
items?.Remove(cacheKey);
}
}
public bool TryGetValue(object key, out object value)
{
var cacheKey = GetCacheKey(key);
var items = httpContextAccessor.HttpContext?.Items;
if (items != null)
{
return items.TryGetValue(cacheKey, out value);
}
value = null;
return false;
}
private static string GetCacheKey(object key)
{
return $"CACHE_{key}";
}
}
}

6
src/Squidex.Infrastructure/Caching/IRequestCache.cs → src/Squidex.Infrastructure/Caching/ILocalCache.cs

@ -5,10 +5,14 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
namespace Squidex.Infrastructure.Caching
{
public interface IRequestCache
public interface ILocalCache
{
IDisposable StartContext();
void Add(object key, object value);
void Remove(object key);

4
src/Squidex.Infrastructure/Caching/RequestCacheExtensions.cs

@ -12,7 +12,7 @@ namespace Squidex.Infrastructure.Caching
{
public static class RequestCacheExtensions
{
public static async Task<T> GetOrCreateAsync<T>(this IRequestCache cache, object key, Func<Task<T>> task)
public static async Task<T> GetOrCreateAsync<T>(this ILocalCache cache, object key, Func<Task<T>> task)
{
if (cache.TryGetValue(key, out var value) && value is T typedValue)
{
@ -26,7 +26,7 @@ namespace Squidex.Infrastructure.Caching
return typedValue;
}
public static T GetOrCreate<T>(this IRequestCache cache, object key, Func<T> task)
public static T GetOrCreate<T>(this ILocalCache cache, object key, Func<T> task)
{
if (cache.TryGetValue(key, out var value) && value is T typedValue)
{

7
src/Squidex.Infrastructure/Commands/DomainObjectGrain.cs

@ -105,13 +105,6 @@ namespace Squidex.Infrastructure.Commands
{
}
public Task WriteSnapshotAsync()
{
snapshot.Version = persistence.Version;
return persistence.WriteSnapshotAsync(snapshot);
}
protected Task<object> CreateReturnAsync<TCommand>(TCommand command, Func<TCommand, Task<object>> handler) where TCommand : class, IAggregateCommand
{
return InvokeAsync(command, handler, false);

2
src/Squidex.Infrastructure/Commands/IDomainObjectGrain.cs

@ -13,8 +13,6 @@ namespace Squidex.Infrastructure.Commands
{
public interface IDomainObjectGrain : IGrainWithGuidKey
{
Task WriteSnapshotAsync();
Task<J<object>> ExecuteAsync(J<IAggregateCommand> command);
}
}

33
src/Squidex.Infrastructure/Orleans/LocalCacheFilter.cs

@ -0,0 +1,33 @@
// ==========================================================================
// 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.Caching;
namespace Squidex.Infrastructure.Orleans
{
public sealed class LocalCacheFilter : IIncomingGrainCallFilter
{
private readonly ILocalCache localCache;
public LocalCacheFilter(ILocalCache localCache)
{
Guard.NotNull(localCache, nameof(localCache));
this.localCache = localCache;
}
public async Task Invoke(IIncomingGrainCallContext context)
{
using (localCache.StartContext())
{
await context.Invoke();
}
}
}
}

1
src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -8,7 +8,6 @@
<DebugSymbols>True</DebugSymbols>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Abstractions" Version="2.0.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="2.0.1" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.1" />
<PackageReference Include="Microsoft.Orleans.Core" Version="2.0.0" />

2
src/Squidex.Infrastructure/States/IStore.cs

@ -18,5 +18,7 @@ namespace Squidex.Infrastructure.States
IPersistence<TState> WithSnapshots<TState>(Type owner, TKey key, Func<TState, Task> applySnapshot);
IPersistence<TState> WithSnapshotsAndEventSourcing<TState>(Type owner, TKey key, Func<TState, Task> applySnapshot, Func<Envelope<IEvent>, Task> applyEvent);
Task ClearSnapshotsAsync<TState>();
}
}

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

@ -57,5 +57,12 @@ namespace Squidex.Infrastructure.States
return new Persistence<TState, TKey>(key, owner, eventStore, eventDataFormatter, snapshotStore, streamNameResolver, mode, applySnapshot, applyEvent);
}
public Task ClearSnapshotsAsync<TState>()
{
var snapshotStore = (ISnapshotStore<TState, TKey>)services.GetService(typeof(ISnapshotStore<TState, TKey>));
return snapshotStore.ClearAsync();
}
}
}

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

@ -172,9 +172,6 @@ namespace Squidex.Config.Domain
services.AddTransientAs<ConvertEventStoreAppId>()
.As<IMigration>();
services.AddTransientAs<DeleteArchiveCollectionSetup>()
.As<IMigration>();
services.AddTransientAs<PopulateGrainIndexes>()
.As<IMigration>();

4
src/Squidex/Config/Domain/InfrastructureServices.cs

@ -32,8 +32,8 @@ namespace Squidex.Config.Domain
services.AddSingletonAs(c => new CachingUsageTracker(c.GetRequiredService<BackgroundUsageTracker>(), c.GetRequiredService<IMemoryCache>()))
.As<IUsageTracker>();
services.AddSingletonAs<HttpRequestCache>()
.As<IRequestCache>();
services.AddSingletonAs<AsyncLocalCache>()
.As<ILocalCache>();
services.AddSingletonAs<HttpContextAccessor>()
.As<IHttpContextAccessor>();

4
src/Squidex/Config/Domain/StoreServices.cs

@ -112,10 +112,10 @@ namespace Squidex.Config.Domain
.As<IEventConsumer>()
.As<IInitializable>();
services.AddTransientAs(c => new DeleteArchiveCollectionSetup(mongoContentDatabase))
services.AddTransientAs<ConvertOldSnapshotStores>()
.As<IMigration>();
services.AddTransientAs<ConvertOldSnapshotStores>()
services.AddTransientAs(c => new DeleteContentCollections(mongoContentDatabase))
.As<IMigration>();
}
});

1
src/Squidex/Config/Orleans/SiloWrapper.cs

@ -55,6 +55,7 @@ namespace Squidex.Config.Orleans
{
var hostBuilder = new SiloHostBuilder()
.UseDashboard(options => options.HostSelf = false)
.AddIncomingGrainCallFilter<LocalCacheFilter>()
.AddStartupTask<Bootstrap<IContentSchedulerGrain>>()
.AddStartupTask<Bootstrap<IEventConsumerManagerGrain>>()
.AddStartupTask<Bootstrap<IRuleDequeuerGrain>>()

7
src/Squidex/Config/Web/WebExtensions.cs

@ -13,6 +13,13 @@ namespace Squidex.Config.Web
{
public static class WebExtensions
{
public static IApplicationBuilder UseMyLocalCache(this IApplicationBuilder app)
{
app.UseMiddleware<LocalCacheMiddleware>();
return app;
}
public static IApplicationBuilder UseMyTracking(this IApplicationBuilder app)
{
app.UseMiddleware<RequestLogPerformanceMiddleware>();

9
src/Squidex/Config/Web/WebServices.cs

@ -24,6 +24,15 @@ namespace Squidex.Config.Web
services.AddSingletonAs<ApiCostsFilter>()
.AsSelf();
services.AddSingletonAs<EnforceHttpsMiddleware>()
.AsSelf();
services.AddSingletonAs<LocalCacheMiddleware>()
.AsSelf();
services.AddSingletonAs<RequestLogPerformanceMiddleware>()
.AsSelf();
services.AddMvc().AddMySerializers();
services.AddCors();
services.AddRouting();

8
src/Squidex/Pipeline/EnforceHttpsMiddleware.cs

@ -13,18 +13,16 @@ using Squidex.Config;
namespace Squidex.Pipeline
{
public sealed class EnforceHttpsMiddleware
public sealed class EnforceHttpsMiddleware : IMiddleware
{
private readonly RequestDelegate next;
private readonly IOptions<MyUrlsOptions> urls;
public EnforceHttpsMiddleware(RequestDelegate next, IOptions<MyUrlsOptions> urls)
public EnforceHttpsMiddleware(IOptions<MyUrlsOptions> urls)
{
this.next = next;
this.urls = urls;
}
public async Task Invoke(HttpContext context)
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
if (!urls.Value.EnforceHTTPS)
{

35
src/Squidex/Pipeline/LocalCacheMiddleware.cs

@ -0,0 +1,35 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
namespace Squidex.Pipeline
{
public sealed class LocalCacheMiddleware : IMiddleware
{
private readonly ILocalCache localCache;
public LocalCacheMiddleware(ILocalCache localCache)
{
Guard.NotNull(localCache, nameof(localCache));
this.localCache = localCache;
}
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
using (localCache.StartContext())
{
await next(context);
}
}
}
}

10
src/Squidex/Pipeline/RequestLogPerformanceMiddleware.cs

@ -8,25 +8,23 @@
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Filters;
using Squidex.Infrastructure.Log;
namespace Squidex.Pipeline
{
public sealed class RequestLogPerformanceMiddleware : ActionFilterAttribute
public sealed class RequestLogPerformanceMiddleware : IMiddleware
{
private readonly RequestLogProfilerSessionProvider requestSession;
private readonly RequestDelegate next;
private readonly ISemanticLog log;
public RequestLogPerformanceMiddleware(RequestLogProfilerSessionProvider requestSession, RequestDelegate next, ISemanticLog log)
public RequestLogPerformanceMiddleware(RequestLogProfilerSessionProvider requestSession, ISemanticLog log)
{
this.requestSession = requestSession;
this.next = next;
this.log = log;
}
public async Task Invoke(HttpContext context)
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var stopWatch = Stopwatch.StartNew();

1
src/Squidex/WebStartup.cs

@ -46,6 +46,7 @@ namespace Squidex
app.ApplicationServices.RunMigrate();
app.ApplicationServices.RunRunnables();
app.UseMyLocalCache();
app.UseMyCors();
app.UseMyForwardingRules();
app.UseMyTracking();

2
src/Squidex/app/features/content/pages/content/content-page.component.html

@ -81,7 +81,7 @@
<ng-container *ngIf="content.status !== 'Archived'">
<button type="button" class="btn btn-secondary" (click)="saveAsProposal()" *ngIf="content.status === 'Published'">
Save as Proposal
Save as Draft
</button>
<button type="submit" class="btn btn-primary" title="CTRL + S">

127
tests/Squidex.Infrastructure.Tests/Caching/AsyncLocalCacheTests.cs

@ -0,0 +1,127 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Xunit;
namespace Squidex.Infrastructure.Caching
{
public class AsyncLocalCacheTests
{
private readonly ILocalCache sut = new AsyncLocalCache();
private int called;
[Fact]
public async Task Should_add_item_to_cache_when_context_exists()
{
using (sut.StartContext())
{
sut.Add("Key", 1);
await Task.Delay(5);
var found = sut.TryGetValue("Key", out var value);
Assert.True(found);
Assert.Equal(1, value);
await Task.Delay(5);
sut.Remove("Key");
var foundAfterRemove = sut.TryGetValue("Key", out value);
Assert.False(foundAfterRemove);
Assert.Null(value);
}
}
[Fact]
public async Task Should_not_add_item_to_cache_when_context_not_exists()
{
sut.Add("Key", 1);
await Task.Delay(5);
var found = sut.TryGetValue("Key", out var value);
Assert.False(found);
Assert.Null(value);
sut.Remove("Key");
await Task.Delay(5);
var foundAfterRemove = sut.TryGetValue("Key", out value);
Assert.False(foundAfterRemove);
Assert.Null(value);
}
[Fact]
public async Task Should_call_creator_once_when_context_exists()
{
using (sut.StartContext())
{
var value1 = sut.GetOrCreate("Key", () => ++called);
await Task.Delay(5);
var value2 = sut.GetOrCreate("Key", () => ++called);
Assert.Equal(1, called);
Assert.Equal(1, value1);
Assert.Equal(1, value2);
}
}
[Fact]
public async Task Should_call_creator_twice_when_context_not_exists()
{
var value1 = sut.GetOrCreate("Key", () => ++called);
await Task.Delay(5);
var value2 = sut.GetOrCreate("Key", () => ++called);
Assert.Equal(2, called);
Assert.Equal(1, value1);
Assert.Equal(2, value2);
}
[Fact]
public async Task Should_call_async_creator_once_when_context_exists()
{
using (sut.StartContext())
{
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
await Task.Delay(5);
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(1, called);
Assert.Equal(1, value1);
Assert.Equal(1, value2);
}
}
[Fact]
public async Task Should_call_async_creator_twice_when_context_not_exists()
{
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
await Task.Delay(5);
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(2, called);
Assert.Equal(1, value1);
Assert.Equal(2, value2);
}
}
}

133
tests/Squidex.Infrastructure.Tests/Caching/HttpRequestCacheTests.cs

@ -1,133 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.AspNetCore.Http;
using Xunit;
namespace Squidex.Infrastructure.Caching
{
public class HttpRequestCacheTests
{
private readonly IHttpContextAccessor httpContextAccessor = A.Fake<IHttpContextAccessor>();
private readonly IRequestCache sut;
private int called;
public HttpRequestCacheTests()
{
sut = new HttpRequestCache(httpContextAccessor);
}
[Fact]
public void Should_add_item_to_cache_when_context_exists()
{
SetupContext();
sut.Add("Key", 1);
var found = sut.TryGetValue("Key", out var value);
Assert.True(found);
Assert.Equal(1, value);
sut.Remove("Key");
var foundAfterRemove = sut.TryGetValue("Key", out value);
Assert.False(foundAfterRemove);
Assert.Null(value);
}
[Fact]
public void Should_not_add_item_to_cache_when_context_not_exists()
{
SetupNoContext();
sut.Add("Key", 1);
var found = sut.TryGetValue("Key", out var value);
Assert.False(found);
Assert.Null(value);
sut.Remove("Key");
var foundAfterRemove = sut.TryGetValue("Key", out value);
Assert.False(foundAfterRemove);
Assert.Null(value);
}
[Fact]
public void Should_call_creator_once_when_context_exists()
{
SetupContext();
var value1 = sut.GetOrCreate("Key", () => ++called);
var value2 = sut.GetOrCreate("Key", () => ++called);
Assert.Equal(1, called);
Assert.Equal(1, value1);
Assert.Equal(1, value2);
}
[Fact]
public void Should_call_creator_twice_when_context_not_exists()
{
SetupNoContext();
var value1 = sut.GetOrCreate("Key", () => ++called);
var value2 = sut.GetOrCreate("Key", () => ++called);
Assert.Equal(2, called);
Assert.Equal(1, value1);
Assert.Equal(2, value2);
}
[Fact]
public async Task Should_call_async_creator_once_when_context_exists()
{
SetupContext();
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(1, called);
Assert.Equal(1, value1);
Assert.Equal(1, value2);
}
[Fact]
public async Task Should_call_async_creator_twice_when_context_not_exists()
{
SetupNoContext();
var value1 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
var value2 = await sut.GetOrCreateAsync("Key", () => Task.FromResult(++called));
Assert.Equal(2, called);
Assert.Equal(1, value1);
Assert.Equal(2, value2);
}
private void SetupNoContext()
{
A.CallTo(() => httpContextAccessor.HttpContext).Returns(null);
}
private void SetupContext()
{
var httpItems = new Dictionary<object, object>();
var httpContext = A.Fake<HttpContext>();
A.CallTo(() => httpContext.Items).Returns(httpItems);
A.CallTo(() => httpContextAccessor.HttpContext).Returns(httpContext);
}
}
}

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

@ -10,8 +10,6 @@ using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.TestHelpers;
using Xunit;
@ -23,8 +21,6 @@ namespace Squidex.Infrastructure.States
private readonly string key = Guid.NewGuid().ToString();
private readonly IEventDataFormatter eventDataFormatter = A.Fake<IEventDataFormatter>();
private readonly IEventStore eventStore = A.Fake<IEventStore>();
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly IPubSub pubSub = new InMemoryPubSub(true);
private readonly IServiceProvider services = A.Fake<IServiceProvider>();
private readonly ISnapshotStore<object, string> snapshotStore = A.Fake<ISnapshotStore<object, string>>();
private readonly IStreamNameResolver streamNameResolver = A.Fake<IStreamNameResolver>();

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

@ -8,8 +8,6 @@
using System;
using System.Threading.Tasks;
using FakeItEasy;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Options;
using Squidex.Infrastructure.EventSourcing;
using Xunit;
@ -22,8 +20,6 @@ namespace Squidex.Infrastructure.States
private readonly string key = Guid.NewGuid().ToString();
private readonly IEventDataFormatter eventDataFormatter = A.Fake<IEventDataFormatter>();
private readonly IEventStore eventStore = A.Fake<IEventStore>();
private readonly IMemoryCache cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
private readonly IPubSub pubSub = new InMemoryPubSub(true);
private readonly IServiceProvider services = A.Fake<IServiceProvider>();
private readonly ISnapshotStore<int, string> snapshotStore = A.Fake<ISnapshotStore<int, string>>();
private readonly IStreamNameResolver streamNameResolver = A.Fake<IStreamNameResolver>();
@ -37,6 +33,15 @@ namespace Squidex.Infrastructure.States
sut = new Store<string>(eventStore, eventDataFormatter, services, streamNameResolver);
}
[Fact]
public async Task Should_call_snapshot_store_on_clear()
{
await sut.ClearSnapshotsAsync<int>();
A.CallTo(() => snapshotStore.ClearAsync())
.MustHaveHappened();
}
[Fact]
public async Task Should_read_from_store()
{

40
tools/Migrate_01/MigrationPath.cs

@ -15,7 +15,7 @@ namespace Migrate_01
{
public sealed class MigrationPath : IMigrationPath
{
private const int CurrentVersion = 10;
private const int CurrentVersion = 11;
private readonly IServiceProvider serviceProvider;
public MigrationPath(IServiceProvider serviceProvider)
@ -32,36 +32,25 @@ namespace Migrate_01
var migrations = new List<IMigration>();
// Version 10: Delete old archive fields
if (version < 10)
{
var migration = serviceProvider.GetService<DeleteArchiveCollectionSetup>();
if (migration != null)
{
migrations.Add(migration);
}
}
// Version 6: Convert Event store. Must always be executed first.
// Version 06: Convert Event store. Must always be executed first.
if (version < 6)
{
migrations.Add(serviceProvider.GetRequiredService<ConvertEventStore>());
}
// Version 7: Introduces AppId for backups.
// Version 07: Introduces AppId for backups.
else if (version < 7)
{
migrations.Add(serviceProvider.GetRequiredService<ConvertEventStoreAppId>());
}
// Version 5: Fixes the broken command architecture and requires a rebuild of all snapshots.
// Version 05: Fixes the broken command architecture and requires a rebuild of all snapshots.
if (version < 5)
{
migrations.Add(serviceProvider.GetRequiredService<RebuildSnapshots>());
}
// Version 9: Grain Indexes
// Version 09: Grain indexes.
if (version < 9)
{
var migration = serviceProvider.GetService<ConvertOldSnapshotStores>();
@ -74,16 +63,23 @@ namespace Migrate_01
migrations.Add(serviceProvider.GetRequiredService<PopulateGrainIndexes>());
}
// Version 1: Introduce App patterns.
if (version <= 1)
// Version 11: Introduce content drafts.
if (version < 11)
{
migrations.Add(serviceProvider.GetRequiredService<AddPatterns>());
var migration = serviceProvider.GetService<DeleteContentCollections>();
if (migration != null)
{
migrations.Add(migration);
}
migrations.Add(serviceProvider.GetRequiredService<RebuildContents>());
}
// Version 8: Introduce Archive collection.
if (version < 8)
// Version 01: Introduce app patterns.
if (version < 1)
{
migrations.Add(serviceProvider.GetRequiredService<DeleteArchiveCollectionSetup>());
migrations.Add(serviceProvider.GetRequiredService<AddPatterns>());
}
return (CurrentVersion, migrations);

2
tools/Migrate_01/Migrations/AddPatterns.cs

@ -53,8 +53,6 @@ namespace Migrate_01.Migrations
await app.ExecuteAsync(command);
}
await app.WriteSnapshotAsync();
}
}
}

32
tools/Migrate_01/Migrations/DeleteArchiveCollection.cs

@ -1,32 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using Squidex.Domain.Apps.Entities.Contents.Repositories;
using Squidex.Domain.Apps.Entities.MongoDb.Contents;
using Squidex.Infrastructure.Migrations;
namespace Migrate_01.Migrations
{
public sealed class DeleteArchiveCollection : IMigration
{
private readonly IContentRepository contentRepository;
public DeleteArchiveCollection(IContentRepository contentRepository)
{
this.contentRepository = contentRepository;
}
public async Task UpdateAsync()
{
if (contentRepository is MongoContentRepository mongoContentRepository)
{
await mongoContentRepository.DeleteArchiveAsync();
}
}
}
}

11
tools/Migrate_01/Migrations/DeleteArchiveCollectionSetup.cs → tools/Migrate_01/Migrations/DeleteContentCollections.cs

@ -6,27 +6,24 @@
// ==========================================================================
using System.Threading.Tasks;
using MongoDB.Bson;
using MongoDB.Driver;
using Squidex.Infrastructure.Migrations;
namespace Migrate_01.Migrations
{
public sealed class DeleteArchiveCollectionSetup : IMigration
public sealed class DeleteContentCollections : IMigration
{
private readonly IMongoDatabase database;
public DeleteArchiveCollectionSetup(IMongoDatabase database)
public DeleteContentCollections(IMongoDatabase database)
{
this.database = database;
}
public async Task UpdateAsync()
{
var collection = database.GetCollection<BsonDocument>("States_Contents");
await collection.Indexes.DropAllAsync();
await collection.UpdateManyAsync(new BsonDocument(), Builders<BsonDocument>.Update.Unset("id"));
await database.DropCollectionAsync("States_Contents");
await database.DropCollectionAsync("States_Contents_Archive");
}
}
}

4
tools/Migrate_01/Migrations/RebuildSnapshots.cs

@ -21,7 +21,9 @@ namespace Migrate_01.Migrations
public async Task UpdateAsync()
{
await rebuilder.RebuildConfigAsync();
await rebuilder.RebuildAppsAsync();
await rebuilder.RebuildSchemasAsync();
await rebuilder.RebuildRulesAsync();
await rebuilder.RebuildContentAsync();
await rebuilder.RebuildAssetsAsync();
}

154
tools/Migrate_01/Rebuilder.cs

@ -9,7 +9,7 @@ using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Orleans;
using System.Threading.Tasks.Dataflow;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Apps.State;
@ -21,11 +21,9 @@ using Squidex.Domain.Apps.Entities.Rules;
using Squidex.Domain.Apps.Entities.Rules.State;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Entities.Schemas.State;
using Squidex.Domain.Apps.Events;
using Squidex.Domain.Apps.Events.Assets;
using Squidex.Domain.Apps.Events.Contents;
using Squidex.Domain.Apps.Events.Rules;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Caching;
using Squidex.Infrastructure.Commands;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.States;
@ -34,137 +32,107 @@ namespace Migrate_01
public sealed class Rebuilder
{
private readonly FieldRegistry fieldRegistry;
private readonly ILocalCache localCache;
private readonly IStore<Guid> store;
private readonly IEventStore eventStore;
private readonly IEventDataFormatter eventDataFormatter;
private readonly ISnapshotStore<AppState, Guid> snapshotAppStore;
private readonly ISnapshotStore<AssetState, Guid> snapshotAssetStore;
private readonly ISnapshotStore<ContentState, Guid> snapshotContentStore;
private readonly ISnapshotStore<RuleState, Guid> snapshotRuleStore;
private readonly ISnapshotStore<SchemaState, Guid> snapshotSchemaStore;
private readonly IGrainFactory grainFactory;
public Rebuilder(
FieldRegistry fieldRegistry,
IEventDataFormatter eventDataFormatter,
IEventStore eventStore,
ISnapshotStore<AppState, Guid> snapshotAppStore,
ISnapshotStore<ContentState, Guid> snapshotContentStore,
ISnapshotStore<AssetState, Guid> snapshotAssetStore,
ISnapshotStore<RuleState, Guid> snapshotRuleStore,
ISnapshotStore<SchemaState, Guid> snapshotSchemaStore,
IGrainFactory grainFactory)
ILocalCache localCache,
IStore<Guid> store,
IEventStore eventStore)
{
this.fieldRegistry = fieldRegistry;
this.eventDataFormatter = eventDataFormatter;
this.eventStore = eventStore;
this.snapshotAppStore = snapshotAppStore;
this.snapshotAssetStore = snapshotAssetStore;
this.snapshotContentStore = snapshotContentStore;
this.snapshotRuleStore = snapshotRuleStore;
this.snapshotSchemaStore = snapshotSchemaStore;
this.grainFactory = grainFactory;
this.localCache = localCache;
this.store = store;
}
public async Task RebuildAssetsAsync()
public async Task RebuildAppsAsync()
{
await snapshotAssetStore.ClearAsync();
await store.ClearSnapshotsAsync<AppState>();
const string filter = "^asset\\-";
await RebuildManyAsync("^app\\-", id => RebuildAsync<AppState, AppGrain>(id, (e, s) => s.Apply(e)));
}
var handledIds = new HashSet<Guid>();
public async Task RebuildSchemasAsync()
{
await store.ClearSnapshotsAsync<SchemaState>();
await eventStore.QueryAsync(async storedEvent =>
{
var @event = ParseKnownEvent(storedEvent);
await RebuildManyAsync("^schema\\-", id => RebuildAsync<SchemaState, SchemaGrain>(id, (e, s) => s.Apply(e, fieldRegistry)));
}
if (@event != null)
{
if (@event.Payload is AssetEvent assetEvent && handledIds.Add(assetEvent.AssetId))
{
var asset = grainFactory.GetGrain<IAssetGrain>(assetEvent.AssetId);
public async Task RebuildRulesAsync()
{
await store.ClearSnapshotsAsync<RuleState>();
await asset.WriteSnapshotAsync();
}
}
}, filter, ct: CancellationToken.None);
await RebuildManyAsync("^rule\\-", id => RebuildAsync<RuleState, RuleGrain>(id, (e, s) => s.Apply(e)));
}
public async Task RebuildConfigAsync()
public async Task RebuildAssetsAsync()
{
await snapshotAppStore.ClearAsync();
await snapshotRuleStore.ClearAsync();
await snapshotSchemaStore.ClearAsync();
const string filter = "^((app\\-)|(schema\\-)|(rule\\-))";
await store.ClearSnapshotsAsync<AssetState>();
var handledIds = new HashSet<Guid>();
await RebuildManyAsync("^asset\\-", id => RebuildAsync<AssetState, AssetGrain>(id, (e, s) => s.Apply(e)));
}
await eventStore.QueryAsync(async storedEvent =>
public async Task RebuildContentAsync()
{
using (localCache.StartContext())
{
var @event = ParseKnownEvent(storedEvent);
await store.ClearSnapshotsAsync<ContentState>();
if (@event != null)
await RebuildManyAsync("^content\\-", async id =>
{
if (@event.Payload is SchemaEvent schemaEvent && handledIds.Add(schemaEvent.SchemaId.Id))
{
var schema = grainFactory.GetGrain<ISchemaGrain>(schemaEvent.SchemaId.Id);
await schema.WriteSnapshotAsync();
}
else if (@event.Payload is RuleEvent ruleEvent && handledIds.Add(ruleEvent.RuleId))
try
{
var rule = grainFactory.GetGrain<IRuleGrain>(ruleEvent.RuleId);
await rule.WriteSnapshotAsync();
await RebuildAsync<ContentState, ContentGrain>(id, (e, s) => s.Apply(e));
}
else if (@event.Payload is AppEvent appEvent && handledIds.Add(appEvent.AppId.Id))
catch (DomainObjectNotFoundException)
{
var app = grainFactory.GetGrain<IAppGrain>(appEvent.AppId.Id);
await app.WriteSnapshotAsync();
return;
}
}
}, filter, ct: CancellationToken.None);
});
}
}
public async Task RebuildContentAsync()
private async Task RebuildManyAsync(string filter, Func<Guid, Task> action)
{
await snapshotContentStore.ClearAsync();
const string filter = "^((content\\-))";
var handledIds = new HashSet<Guid>();
var worker = new ActionBlock<Guid>(action, new ExecutionDataflowBlockOptions { MaxDegreeOfParallelism = 32 });
await eventStore.QueryAsync(async storedEvent =>
{
var @event = ParseKnownEvent(storedEvent);
var id = Guid.Parse(storedEvent.Data.Metadata.Value<string>(CommonHeaders.AggregateId));
if (@event.Payload is ContentEvent contentEvent && handledIds.Add(contentEvent.ContentId))
if (handledIds.Add(id))
{
try
{
var content = grainFactory.GetGrain<IContentGrain>(contentEvent.ContentId);
await content.WriteSnapshotAsync();
}
catch (DomainObjectNotFoundException)
{
// Schema has been deleted.
}
await worker.SendAsync(id);
}
}, filter, ct: CancellationToken.None);
worker.Complete();
await worker.Completion;
}
private Envelope<IEvent> ParseKnownEvent(StoredEvent storedEvent)
private async Task RebuildAsync<TState, TGrain>(Guid key, Func<Envelope<IEvent>, TState, TState> func) where TState : IDomainState, new()
{
try
var state = new TState
{
return eventDataFormatter.Parse(storedEvent.Data);
}
catch (TypeNameNotFoundException)
Version = EtagVersion.Empty
};
var persistence = store.WithSnapshotsAndEventSourcing<TState, Guid>(typeof(TGrain), key, s => state = s, e =>
{
return null;
}
state = func(e, state);
state.Version++;
});
await persistence.ReadAsync();
await persistence.WriteSnapshotAsync(state);
}
}
}
Loading…
Cancel
Save