Browse Source

Background cache. (#634)

pull/636/head
Sebastian Stehle 5 years ago
committed by GitHub
parent
commit
19a85d4391
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs
  2. 9
      backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs
  3. 5
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndexerState.cs
  4. 145
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs
  5. 32
      backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHashEntity.cs
  6. 71
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs
  7. 56
      backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs
  8. 28
      backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs
  9. 22
      backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemasHash.cs
  10. 5
      backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj
  11. 29
      backend/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs
  12. 73
      backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs
  13. 36
      backend/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs
  14. 1
      backend/src/Squidex/Config/Domain/InfrastructureServices.cs
  15. 3
      backend/src/Squidex/Config/Domain/QueryServices.cs
  16. 6
      backend/src/Squidex/Config/Domain/StoreServices.cs
  17. 2
      backend/src/Squidex/Squidex.csproj
  18. 3
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs
  19. 8
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs
  20. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs
  21. 38
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs
  22. 110
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashTests.cs
  23. 2
      backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj
  24. 38
      backend/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs
  25. 38
      backend/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs

9
backend/src/Migrations/Migrations/MongoDb/AddAppIdToEventStream.cs

@ -46,7 +46,7 @@ namespace Migrations.Migrations.MongoDb
var actionBlock = new ActionBlock<BsonDocument[]>(async batch =>
{
var updates = new List<WriteModel<BsonDocument>>();
var writes = new List<WriteModel<BsonDocument>>();
foreach (var document in batch)
{
@ -92,13 +92,16 @@ namespace Migrations.Migrations.MongoDb
var filter = Builders<BsonDocument>.Filter.Eq("_id", document["_id"].AsString);
updates.Add(new ReplaceOneModel<BsonDocument>(filter, document)
writes.Add(new ReplaceOneModel<BsonDocument>(filter, document)
{
IsUpsert = true
});
}
await collectionNew.BulkWriteAsync(updates, writeOptions);
if (writes.Count > 0)
{
await collectionNew.BulkWriteAsync(writes, writeOptions);
}
}, new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount * 2,

9
backend/src/Migrations/Migrations/MongoDb/ConvertDocumentIds.cs

@ -102,7 +102,7 @@ namespace Migrations.Migrations.MongoDb
var actionBlock = new ActionBlock<BsonDocument[]>(async batch =>
{
var updates = new List<WriteModel<BsonDocument>>();
var writes = new List<WriteModel<BsonDocument>>();
foreach (var document in batch)
{
@ -126,13 +126,16 @@ namespace Migrations.Migrations.MongoDb
var filter = Builders<BsonDocument>.Filter.Eq("_id", documentIdNew);
updates.Add(new ReplaceOneModel<BsonDocument>(filter, document)
writes.Add(new ReplaceOneModel<BsonDocument>(filter, document)
{
IsUpsert = true
});
}
await collectionNew.BulkWriteAsync(updates, writeOptions);
if (writes.Count > 0)
{
await collectionNew.BulkWriteAsync(writes, writeOptions);
}
}, new ExecutionDataflowBlockOptions
{
MaxDegreeOfParallelism = Environment.ProcessorCount * 2,

5
backend/src/Squidex.Domain.Apps.Entities.MongoDb/FullText/MongoTextIndexerState.cs

@ -75,6 +75,11 @@ namespace Squidex.Domain.Apps.Entities.MongoDb.FullText
}
}
if (writes.Count == 0)
{
return Task.CompletedTask;
}
return Collection.BulkWriteAsync(writes);
}
}

145
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHash.cs

@ -0,0 +1,145 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using MongoDB.Driver;
using NodaTime;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Apps.Events;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.MongoDb;
using Squidex.Infrastructure.ObjectPool;
namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas
{
public sealed class MongoSchemasHash : MongoRepositoryBase<MongoSchemasHashEntity>, ISchemasHash, IEventConsumer
{
public int BatchSize
{
get { return 1000; }
}
public int BatchDelay
{
get { return 500; }
}
public string Name
{
get { return GetType().Name; }
}
public string EventsFilter
{
get { return "^(app-|schema-)"; }
}
public MongoSchemasHash(IMongoDatabase database, bool setup = false)
: base(database, setup)
{
}
protected override string CollectionName()
{
return "SchemasHash";
}
public Task On(IEnumerable<Envelope<IEvent>> events)
{
var writes = new List<WriteModel<MongoSchemasHashEntity>>();
foreach (var @event in events)
{
switch (@event.Payload)
{
case SchemaEvent schemaEvent:
{
writes.Add(
new UpdateOneModel<MongoSchemasHashEntity>(
Filter.Eq(x => x.AppId, schemaEvent.AppId.Id.ToString()),
Update
.Set($"s.{schemaEvent.SchemaId.Id}", @event.Headers.EventStreamNumber())
.Set(x => x.Updated, @event.Headers.Timestamp())));
break;
}
case AppEvent appEvent:
writes.Add(
new UpdateOneModel<MongoSchemasHashEntity>(
Filter.Eq(x => x.AppId, appEvent.AppId.Id.ToString()),
Update
.Set(x => x.AppVersion, @event.Headers.EventStreamNumber())
.Set(x => x.Updated, @event.Headers.Timestamp()))
{
IsUpsert = true
});
break;
}
}
if (writes.Count == 0)
{
return Task.CompletedTask;
}
return Collection.BulkWriteAsync(writes);
}
public async Task<(Instant Create, string Hash)> GetCurrentHashAsync(DomainId appId)
{
var entity = await Collection.Find(x => x.AppId == appId.ToString()).FirstOrDefaultAsync();
if (entity == null)
{
return (default, string.Empty);
}
var ids =
entity.SchemaVersions.Select(x => (x.Key, x.Value))
.Union(Enumerable.Repeat((entity.AppId, entity.AppVersion), 1));
var hash = CreateHash(ids);
return (entity.Updated, hash);
}
public ValueTask<string> ComputeHashAsync(IAppEntity app, IEnumerable<ISchemaEntity> schemas)
{
var ids =
schemas.Select(x => (x.Id.ToString(), x.Version))
.Union(Enumerable.Repeat((app.Id.ToString(), app.Version), 1));
var hash = CreateHash(ids);
return new ValueTask<string>(hash);
}
private static string CreateHash(IEnumerable<(string, long)> ids)
{
var sb = DefaultPools.StringBuilder.Get();
try
{
foreach (var (id, version) in ids.OrderBy(x => x.Item1))
{
sb.Append(id);
sb.Append(version);
sb.Append(';');
}
return sb.ToString().Sha256Base64();
}
finally
{
DefaultPools.StringBuilder.Return(sb);
}
}
}
}

32
backend/src/Squidex.Domain.Apps.Entities.MongoDb/Schemas/MongoSchemasHashEntity.cs

@ -0,0 +1,32 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Collections.Generic;
using MongoDB.Bson.Serialization.Attributes;
using NodaTime;
namespace Squidex.Domain.Apps.Entities.MongoDb.Schemas
{
public sealed class MongoSchemasHashEntity
{
[BsonId]
[BsonElement]
public string AppId { get; set; }
[BsonRequired]
[BsonElement("v")]
public long AppVersion { get; set; }
[BsonRequired]
[BsonElement("s")]
public Dictionary<string, long> SchemaVersions { get; set; }
[BsonRequired]
[BsonElement("t")]
public Instant Updated { get; set; }
}
}

71
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/CachingGraphQLService.cs

@ -8,31 +8,40 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using GraphQL.Utilities;
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using NodaTime;
using Squidex.Caching;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Entities.Contents.GraphQL.Types;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Infrastructure;
using Squidex.Log;
#pragma warning disable SA1313 // Parameter names should begin with lower-case letter
namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
{
public sealed class CachingGraphQLService : IGraphQLService
{
private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(10);
private readonly IMemoryCache cache;
private readonly IServiceProvider resolver;
private readonly IBackgroundCache cache;
private readonly ISchemasHash schemasHash;
private readonly IServiceProvider serviceProvider;
private readonly GraphQLOptions options;
public CachingGraphQLService(IMemoryCache cache, IServiceProvider resolver, IOptions<GraphQLOptions> options)
public sealed record CacheEntry(GraphQLModel Model, string Hash, Instant Created);
public CachingGraphQLService(IBackgroundCache cache, ISchemasHash schemasHash, IServiceProvider serviceProvider, IOptions<GraphQLOptions> options)
{
Guard.NotNull(cache, nameof(cache));
Guard.NotNull(resolver, nameof(resolver));
Guard.NotNull(schemasHash, nameof(schemasHash));
Guard.NotNull(serviceProvider, nameof(serviceProvider));
Guard.NotNull(options, nameof(options));
this.cache = cache;
this.resolver = resolver;
this.schemasHash = schemasHash;
this.serviceProvider = serviceProvider;
this.options = options.Value;
}
@ -43,9 +52,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var model = await GetModelAsync(context.App);
var graphQlContext = new GraphQLExecutionContext(context, resolver);
var executionContext =
serviceProvider.GetRequiredService<GraphQLExecutionContext>()
.WithContext(context);
var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, graphQlContext, q)));
var result = await Task.WhenAll(queries.Select(q => QueryInternalAsync(model, executionContext, q)));
return (result.Any(x => x.HasError), result.Map(x => x.Response));
}
@ -57,9 +68,11 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var model = await GetModelAsync(context.App);
var graphQlContext = new GraphQLExecutionContext(context, resolver);
var executionContext =
serviceProvider.GetRequiredService<GraphQLExecutionContext>()
.WithContext(context);
var result = await QueryInternalAsync(model, graphQlContext, query);
var result = await QueryInternalAsync(model, executionContext, query);
return result;
}
@ -83,7 +96,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
}
}
private Task<GraphQLModel> GetModelAsync(IAppEntity app)
private async Task<GraphQLModel> GetModelAsync(IAppEntity app)
{
var entry = await GetModelEntryAsync(app);
return entry.Model;
}
private Task<CacheEntry> GetModelEntryAsync(IAppEntity app)
{
if (options.CacheDuration <= 0)
{
@ -92,22 +112,31 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var cacheKey = CreateCacheKey(app.Id, app.Version.ToString());
return cache.GetOrCreateAsync(cacheKey, async entry =>
return cache.GetOrCreateAsync(cacheKey, CacheDuration, async entry =>
{
entry.AbsoluteExpirationRelativeToNow = CacheDuration;
return await CreateModelAsync(app);
},
async entry =>
{
var (created, hash) = await schemasHash.GetCurrentHashAsync(app.Id);
return created < entry.Created || string.Equals(hash, entry.Hash, StringComparison.OrdinalIgnoreCase);
});
}
private async Task<GraphQLModel> CreateModelAsync(IAppEntity app)
private async Task<CacheEntry> CreateModelAsync(IAppEntity app)
{
var allSchemas = await resolver.GetRequiredService<IAppProvider>().GetSchemasAsync(app.Id);
var allSchemas = await serviceProvider.GetRequiredService<IAppProvider>().GetSchemasAsync(app.Id);
var hash = await schemasHash.ComputeHashAsync(app, allSchemas);
return new GraphQLModel(app,
allSchemas,
resolver.GetRequiredService<SharedTypes>(),
resolver.GetRequiredService<ISemanticLog>());
return new CacheEntry(
new GraphQLModel(app,
allSchemas,
serviceProvider.GetRequiredService<SharedTypes>(),
serviceProvider.GetRequiredService<ISemanticLog>()),
hash,
SystemClock.Instance.GetCurrentInstant());
}
private static object CreateCacheKey(DomainId appId, string etag)

56
backend/src/Squidex.Domain.Apps.Entities/Contents/GraphQL/GraphQLExecutionContext.cs

@ -5,13 +5,11 @@
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using GraphQL;
using GraphQL.DataLoader;
using GraphQL.Utilities;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Entities.Assets;
using Squidex.Domain.Apps.Entities.Contents.Queries;
@ -27,37 +25,53 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private static readonly List<IEnrichedAssetEntity> EmptyAssets = new List<IEnrichedAssetEntity>();
private static readonly List<IEnrichedContentEntity> EmptyContents = new List<IEnrichedContentEntity>();
private readonly IDataLoaderContextAccessor dataLoaderContextAccessor;
private readonly IServiceProvider resolver;
private readonly DataLoaderDocumentListener dataLoaderDocumentListener;
private readonly IUrlGenerator urlGenerator;
private readonly ISemanticLog log;
private readonly ICommandBus commandBus;
private Context context;
public IUrlGenerator UrlGenerator { get; }
public ICommandBus CommandBus { get; }
public IUrlGenerator UrlGenerator
{
get { return urlGenerator; }
}
public ISemanticLog Log { get; }
public ICommandBus CommandBus
{
get { return commandBus; }
}
public GraphQLExecutionContext(Context context, IServiceProvider resolver)
: base(context
.WithoutCleanup()
.WithoutContentEnrichment(),
resolver.GetRequiredService<IAssetQueryService>(),
resolver.GetRequiredService<IContentQueryService>())
public ISemanticLog Log
{
UrlGenerator = resolver.GetRequiredService<IUrlGenerator>();
get { return log; }
}
CommandBus = resolver.GetRequiredService<ICommandBus>();
public override Context Context
{
get { return context; }
}
Log = resolver.GetRequiredService<ISemanticLog>();
public GraphQLExecutionContext(IAssetQueryService assetQuery, IContentQueryService contentQuery,
IDataLoaderContextAccessor dataLoaderContextAccessor, DataLoaderDocumentListener dataLoaderDocumentListener, ICommandBus commandBus, IUrlGenerator urlGenerator, ISemanticLog log)
: base(assetQuery, contentQuery)
{
this.commandBus = commandBus;
this.dataLoaderContextAccessor = dataLoaderContextAccessor;
this.dataLoaderDocumentListener = dataLoaderDocumentListener;
this.urlGenerator = urlGenerator;
this.log = log;
}
dataLoaderContextAccessor = resolver.GetRequiredService<IDataLoaderContextAccessor>();
public GraphQLExecutionContext WithContext(Context newContext)
{
context = newContext.WithoutCleanup().WithoutContentEnrichment();
this.resolver = resolver;
return this;
}
public void Setup(ExecutionOptions execution)
{
var loader = resolver.GetRequiredService<DataLoaderDocumentListener>();
execution.Listeners.Add(loader);
execution.Listeners.Add(dataLoaderDocumentListener);
execution.UserContext = this;
}

28
backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/QueryExecutionContext.cs

@ -15,34 +15,28 @@ using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Contents.Queries
{
public class QueryExecutionContext : Dictionary<string, object>
public abstract class QueryExecutionContext : Dictionary<string, object>
{
private readonly SemaphoreSlim maxRequests = new SemaphoreSlim(10);
private readonly ConcurrentDictionary<DomainId, IEnrichedContentEntity?> cachedContents = new ConcurrentDictionary<DomainId, IEnrichedContentEntity?>();
private readonly ConcurrentDictionary<DomainId, IEnrichedAssetEntity?> cachedAssets = new ConcurrentDictionary<DomainId, IEnrichedAssetEntity?>();
private readonly IContentQueryService contentQuery;
private readonly IAssetQueryService assetQuery;
private readonly Context context;
public Context Context
{
get { return context; }
}
public abstract Context Context { get; }
public QueryExecutionContext(Context context, IAssetQueryService assetQuery, IContentQueryService contentQuery)
protected QueryExecutionContext(IAssetQueryService assetQuery, IContentQueryService contentQuery)
{
Guard.NotNull(assetQuery, nameof(assetQuery));
Guard.NotNull(contentQuery, nameof(contentQuery));
Guard.NotNull(context, nameof(context));
this.assetQuery = assetQuery;
this.contentQuery = contentQuery;
this.context = context;
}
public virtual Task<IEnrichedContentEntity?> FindContentAsync(string schemaIdOrName, DomainId id, long version)
{
return contentQuery.FindAsync(context, schemaIdOrName, id, version);
return contentQuery.FindAsync(Context, schemaIdOrName, id, version);
}
public virtual async Task<IEnrichedAssetEntity?> FindAssetAsync(DomainId id)
@ -54,7 +48,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync();
try
{
asset = await assetQuery.FindAsync(context, id);
asset = await assetQuery.FindAsync(Context, id);
}
finally
{
@ -79,7 +73,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync();
try
{
content = await contentQuery.FindAsync(context, schemaId.ToString(), id);
content = await contentQuery.FindAsync(Context, schemaId.ToString(), id);
}
finally
{
@ -104,7 +98,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync();
try
{
assets = await assetQuery.QueryAsync(context, null, q);
assets = await assetQuery.QueryAsync(Context, null, q);
}
finally
{
@ -128,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync();
try
{
contents = await contentQuery.QueryAsync(context, schemaIdOrName, q);
contents = await contentQuery.QueryAsync(Context, schemaIdOrName, q);
}
finally
{
@ -156,7 +150,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync();
try
{
assets = await assetQuery.QueryAsync(context, null, Q.Empty.WithIds(notLoadedAssets));
assets = await assetQuery.QueryAsync(Context, null, Q.Empty.WithIds(notLoadedAssets));
}
finally
{
@ -185,7 +179,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync();
try
{
contents = await contentQuery.QueryAsync(context, Q.Empty.WithIds(notLoadedContents));
contents = await contentQuery.QueryAsync(Context, Q.Empty.WithIds(notLoadedContents));
}
finally
{
@ -208,7 +202,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries
await maxRequests.WaitAsync();
try
{
return await contentQuery.QueryAsync(context, schemaIdOrName, q);
return await contentQuery.QueryAsync(Context, schemaIdOrName, q);
}
finally
{

22
backend/src/Squidex.Domain.Apps.Entities/Schemas/ISchemasHash.cs

@ -0,0 +1,22 @@
// ==========================================================================
// 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 NodaTime;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Infrastructure;
namespace Squidex.Domain.Apps.Entities.Schemas
{
public interface ISchemasHash
{
Task<(Instant Create, string Hash)> GetCurrentHashAsync(DomainId appId);
ValueTask<string> ComputeHashAsync(IAppEntity app, IEnumerable<ISchemaEntity> schemas);
}
}

5
backend/src/Squidex.Infrastructure/Squidex.Infrastructure.csproj

@ -25,7 +25,7 @@
<PackageReference Include="NJsonSchema" Version="10.3.2" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Assets" Version="1.3.0" />
<PackageReference Include="Squidex.Caching" Version="1.3.0" />
<PackageReference Include="Squidex.Caching" Version="1.6.0" />
<PackageReference Include="Squidex.Hosting.Abstractions" Version="1.8.0" />
<PackageReference Include="Squidex.Log" Version="1.1.0" />
<PackageReference Include="Squidex.Text" Version="1.5.0" />
@ -45,7 +45,4 @@
<ItemGroup>
<AdditionalFiles Include="..\..\stylecop.json" Link="stylecop.json" />
</ItemGroup>
<ItemGroup>
<Folder Include="Pool\" />
</ItemGroup>
</Project>

29
backend/src/Squidex.Infrastructure/Tasks/AsyncLocalCleaner.cs

@ -1,29 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
namespace Squidex.Infrastructure.Tasks
{
public sealed class AsyncLocalCleaner<T> : IDisposable
{
private readonly AsyncLocal<T> asyncLocal;
public AsyncLocalCleaner(AsyncLocal<T> asyncLocal)
{
Guard.NotNull(asyncLocal, nameof(asyncLocal));
this.asyncLocal = asyncLocal;
}
public void Dispose()
{
asyncLocal.Value = default!;
}
}
}

73
backend/src/Squidex.Infrastructure/Tasks/AsyncLock.cs

@ -1,73 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading;
using System.Threading.Tasks;
#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body
namespace Squidex.Infrastructure.Tasks
{
public sealed class AsyncLock
{
private readonly SemaphoreSlim semaphore;
public AsyncLock()
{
semaphore = new SemaphoreSlim(1);
}
public Task<IDisposable> LockAsync()
{
var wait = semaphore.WaitAsync();
if (wait.IsCompleted)
{
return Task.FromResult((IDisposable)new LockReleaser(this));
}
else
{
return wait.ContinueWith(x => (IDisposable)new LockReleaser(this),
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
}
private class LockReleaser : IDisposable
{
private AsyncLock? target;
internal LockReleaser(AsyncLock target)
{
this.target = target;
}
public void Dispose()
{
var current = target;
if (current == null)
{
return;
}
target = null;
try
{
current.semaphore.Release();
}
catch
{
// just ignore the Exception
}
}
}
}
}

36
backend/src/Squidex.Infrastructure/Tasks/AsyncLockPool.cs

@ -1,36 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschränkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Threading.Tasks;
namespace Squidex.Infrastructure.Tasks
{
public sealed class AsyncLockPool
{
private readonly AsyncLock[] locks;
public AsyncLockPool(int poolSize)
{
Guard.GreaterThan(poolSize, 0, nameof(poolSize));
locks = new AsyncLock[poolSize];
for (var i = 0; i < poolSize; i++)
{
locks[i] = new AsyncLock();
}
}
public Task<IDisposable> LockAsync(object target)
{
Guard.NotNull(target, nameof(target));
return locks[Math.Abs(target.GetHashCode() % locks.Length)].LockAsync();
}
}
}

1
backend/src/Squidex/Config/Domain/InfrastructureServices.cs

@ -48,6 +48,7 @@ namespace Squidex.Config.Domain
services.AddReplicatedCache();
services.AddAsyncLocalCache();
services.AddBackgroundCache();
services.AddSingletonAs(_ => SystemClock.Instance)
.As<IClock>();

3
backend/src/Squidex/Config/Domain/QueryServices.cs

@ -29,6 +29,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs<DataLoaderContextAccessor>()
.As<IDataLoaderContextAccessor>();
services.AddTransientAs<GraphQLExecutionContext>()
.AsSelf();
services.AddSingletonAs<DataLoaderDocumentListener>()
.AsSelf();

6
backend/src/Squidex/Config/Domain/StoreServices.cs

@ -25,12 +25,15 @@ using Squidex.Domain.Apps.Entities.MongoDb.Contents;
using Squidex.Domain.Apps.Entities.MongoDb.FullText;
using Squidex.Domain.Apps.Entities.MongoDb.History;
using Squidex.Domain.Apps.Entities.MongoDb.Rules;
using Squidex.Domain.Apps.Entities.MongoDb.Schemas;
using Squidex.Domain.Apps.Entities.Rules.Repositories;
using Squidex.Domain.Apps.Entities.Schemas;
using Squidex.Domain.Users;
using Squidex.Domain.Users.MongoDb;
using Squidex.Domain.Users.MongoDb.Infrastructure;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Diagnostics;
using Squidex.Infrastructure.EventSourcing;
using Squidex.Infrastructure.Log;
using Squidex.Infrastructure.Migrations;
using Squidex.Infrastructure.States;
@ -115,6 +118,9 @@ namespace Squidex.Config.Domain
services.AddSingletonAs(c => ActivatorUtilities.CreateInstance<MongoContentRepository>(c, GetDatabase(c, mongoContentDatabaseName)))
.As<IContentRepository>().As<ISnapshotStore<ContentDomainObject.State, DomainId>>();
services.AddSingletonAs<MongoSchemasHash>()
.AsOptional<ISchemasHash>().As<IEventConsumer>();
services.AddSingletonAs<MongoTextIndex>()
.AsOptional<ITextIndex>();

2
backend/src/Squidex/Squidex.csproj

@ -63,7 +63,7 @@
<PackageReference Include="Squidex.Assets.FTP" Version="1.3.0" />
<PackageReference Include="Squidex.Assets.Mongo" Version="1.3.0" />
<PackageReference Include="Squidex.Assets.S3" Version="1.3.0" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.3.0" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.5.0" />
<PackageReference Include="Squidex.ClientLibrary" Version="6.8.0" />
<PackageReference Include="Squidex.Hosting" Version="1.8.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />

3
backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/MongoDb/AssetsQueryFixture.cs

@ -13,7 +13,6 @@ using MongoDB.Driver;
using Newtonsoft.Json;
using Squidex.Domain.Apps.Core.Assets;
using Squidex.Domain.Apps.Core.TestHelpers;
using Squidex.Domain.Apps.Entities.Assets.Repositories;
using Squidex.Domain.Apps.Entities.MongoDb.Assets;
using Squidex.Infrastructure;
using Squidex.Infrastructure.Json.Objects;
@ -28,7 +27,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.MongoDb
private readonly IMongoClient mongoClient = new MongoClient("mongodb://localhost");
private readonly IMongoDatabase mongoDatabase;
public IAssetRepository AssetRepository { get; }
public MongoAssetRepository AssetRepository { get; }
public NamedId<DomainId>[] AppIds { get; } =
{

8
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/GraphQL/GraphQLTestBase.cs

@ -13,6 +13,7 @@ using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Newtonsoft.Json;
using Squidex.Caching;
using Squidex.Domain.Apps.Core;
using Squidex.Domain.Apps.Core.Schemas;
using Squidex.Domain.Apps.Core.TestHelpers;
@ -142,7 +143,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
private CachingGraphQLService CreateSut()
{
var cache = new MemoryCache(Options.Create(new MemoryCacheOptions()));
var cache = new BackgroundCache(new MemoryCache(Options.Create(new MemoryCacheOptions())));
var appProvider = A.Fake<IAppProvider>();
@ -161,6 +162,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
var services =
new ServiceCollection()
.AddMemoryCache()
.AddTransient<GraphQLExecutionContext>()
.AddSingleton(A.Fake<ISemanticLog>())
.AddSingleton(appProvider)
.AddSingleton(assetQuery)
@ -173,7 +175,9 @@ namespace Squidex.Domain.Apps.Entities.Contents.GraphQL
FakeUrlGenerator>()
.BuildServiceProvider();
return new CachingGraphQLService(cache, services, Options.Create(new GraphQLOptions()));
var schemasHash = A.Fake<ISchemasHash>();
return new CachingGraphQLService(cache, schemasHash, services, Options.Create(new GraphQLOptions()));
}
}
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/MongoDb/ContentsQueryFixture.cs

@ -35,7 +35,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.MongoDb
private readonly IMongoClient mongoClient = new MongoClient("mongodb://localhost");
private readonly IMongoDatabase mongoDatabase;
public IContentRepository ContentRepository { get; }
public MongoContentRepository ContentRepository { get; }
public NamedId<DomainId>[] AppIds { get; } =
{

38
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashFixture.cs

@ -0,0 +1,38 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using MongoDB.Driver;
using Squidex.Domain.Apps.Entities.MongoDb.Schemas;
using Squidex.Infrastructure.MongoDb;
namespace Squidex.Domain.Apps.Entities.Schemas.MongoDb
{
public sealed class SchemasHashFixture
{
private readonly IMongoClient mongoClient = new MongoClient("mongodb://localhost");
private readonly IMongoDatabase mongoDatabase;
public MongoSchemasHash SchemasHash { get; }
public SchemasHashFixture()
{
InstantSerializer.Register();
mongoDatabase = mongoClient.GetDatabase("QueryTests");
var schemasHash = new MongoSchemasHash(mongoDatabase);
Task.Run(async () =>
{
await schemasHash.InitializeAsync();
}).Wait();
SchemasHash = schemasHash;
}
}
}

110
backend/tests/Squidex.Domain.Apps.Entities.Tests/Schemas/MongoDb/SchemasHashTests.cs

@ -0,0 +1,110 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System.Threading.Tasks;
using FakeItEasy;
using NodaTime;
using Squidex.Domain.Apps.Entities.Apps;
using Squidex.Domain.Apps.Events.Apps;
using Squidex.Domain.Apps.Events.Schemas;
using Squidex.Infrastructure;
using Squidex.Infrastructure.EventSourcing;
using Xunit;
#pragma warning disable SA1300 // Element should begin with upper-case letter
namespace Squidex.Domain.Apps.Entities.Schemas.MongoDb
{
[Trait("Category", "Dependencies")]
public class SchemasHashTests : IClassFixture<SchemasHashFixture>
{
public SchemasHashFixture _ { get; }
public SchemasHashTests(SchemasHashFixture fixture)
{
_ = fixture;
}
[Fact]
public async Task Should_compute_cache_independent_from_order()
{
var app = CreateApp(DomainId.NewGuid(), 1);
var schema1 = CreateSchema(DomainId.NewGuid(), 2);
var schema2 = CreateSchema(DomainId.NewGuid(), 3);
var hash1 = await _.SchemasHash.ComputeHashAsync(app, new[] { schema1, schema2 });
var hash2 = await _.SchemasHash.ComputeHashAsync(app, new[] { schema2, schema1 });
Assert.NotNull(hash1);
Assert.NotNull(hash2);
Assert.Equal(hash1, hash2);
}
[Fact]
public async Task Should_compute_cache_independent_from_db()
{
var app = CreateApp(DomainId.NewGuid(), 1);
var schema1 = CreateSchema(DomainId.NewGuid(), 2);
var schema2 = CreateSchema(DomainId.NewGuid(), 3);
var timestamp = SystemClock.Instance.GetCurrentInstant().WithoutMs();
var computedHash = await _.SchemasHash.ComputeHashAsync(app, new[] { schema1, schema2 });
await _.SchemasHash.On(new[]
{
Envelope.Create<IEvent>(new AppCreated
{
AppId = NamedId.Of(app.Id, "my-app")
}).SetEventStreamNumber(app.Version).SetTimestamp(timestamp),
Envelope.Create<IEvent>(new SchemaCreated
{
AppId = NamedId.Of(app.Id, "my-app"),
SchemaId = NamedId.Of(schema1.Id, "my-schema")
}).SetEventStreamNumber(schema1.Version).SetTimestamp(timestamp),
Envelope.Create<IEvent>(new SchemaCreated
{
AppId = NamedId.Of(app.Id, "my-app"),
SchemaId = NamedId.Of(schema2.Id, "my-schema")
}).SetEventStreamNumber(schema2.Version).SetTimestamp(timestamp)
});
var (dbTime, dbHash) = await _.SchemasHash.GetCurrentHashAsync(app.Id);
Assert.Equal(dbHash, computedHash);
Assert.Equal(dbTime, timestamp);
}
private static IAppEntity CreateApp(DomainId id, long version)
{
var app = A.Fake<IAppEntity>();
A.CallTo(() => app.Id)
.Returns(id);
A.CallTo(() => app.Version)
.Returns(version);
return app;
}
private static ISchemaEntity CreateSchema(DomainId id, long version)
{
var schema = A.Fake<ISchemaEntity>();
A.CallTo(() => schema.Id)
.Returns(id);
A.CallTo(() => schema.Version)
.Returns(version);
return schema;
}
}
}

2
backend/tests/Squidex.Domain.Apps.Entities.Tests/Squidex.Domain.Apps.Entities.Tests.csproj

@ -27,7 +27,7 @@
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="Microsoft.Orleans.TestingHost" Version="3.4.0" />
<PackageReference Include="RefactoringEssentials" Version="5.6.0" PrivateAssets="all" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.3.0" />
<PackageReference Include="Squidex.Caching.Orleans" Version="1.5.0" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118" PrivateAssets="all" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="xunit" Version="2.4.1" />

38
backend/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockPoolTests.cs

@ -1,38 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace Squidex.Infrastructure.Tasks
{
public class AsyncLockPoolTests
{
[Fact]
public async Task Should_lock()
{
var sut = new AsyncLockPool(1);
var value = 0;
await Task.WhenAll(
Enumerable.Repeat(0, 100).Select(x => new Func<Task>(async () =>
{
using (await sut.LockAsync(1))
{
await Task.Yield();
value++;
}
})()));
Assert.Equal(100, value);
}
}
}

38
backend/tests/Squidex.Infrastructure.Tests/Tasks/AsyncLockTests.cs

@ -1,38 +0,0 @@
// ==========================================================================
// Squidex Headless CMS
// ==========================================================================
// Copyright (c) Squidex UG (haftungsbeschraenkt)
// All rights reserved. Licensed under the MIT license.
// ==========================================================================
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace Squidex.Infrastructure.Tasks
{
public class AsyncLockTests
{
[Fact]
public async Task Should_lock()
{
var sut = new AsyncLock();
var value = 0;
await Task.WhenAll(
Enumerable.Repeat(0, 100).Select(x => new Func<Task>(async () =>
{
using (await sut.LockAsync())
{
await Task.Yield();
value++;
}
})()));
Assert.Equal(100, value);
}
}
}
Loading…
Cancel
Save