diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs index 099a24d90..ad8608f92 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/AssetOptions.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; + namespace Squidex.Domain.Apps.Entities.Assets { public sealed class AssetOptions @@ -14,5 +16,9 @@ namespace Squidex.Domain.Apps.Entities.Assets public int MaxResults { get; set; } = 200; public long MaxSize { get; set; } = 5 * 1024 * 1024; + + public TimeSpan TimeoutFind { get; set; } = TimeSpan.FromSeconds(1); + + public TimeSpan TimeoutQuery { get; set; } = TimeSpan.FromSeconds(5); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs index e82d7ece2..e6e435bd9 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Assets/Queries/AssetQueryService.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Infrastructure; using Squidex.Log; @@ -22,6 +23,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries private readonly IAssetRepository assetRepository; private readonly IAssetLoader assetLoader; private readonly IAssetFolderRepository assetFolderRepository; + private readonly AssetOptions options; private readonly AssetQueryParser queryParser; public AssetQueryService( @@ -29,18 +31,21 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries IAssetRepository assetRepository, IAssetLoader assetLoader, IAssetFolderRepository assetFolderRepository, + IOptions options, AssetQueryParser queryParser) { Guard.NotNull(assetEnricher, nameof(assetEnricher)); Guard.NotNull(assetRepository, nameof(assetRepository)); Guard.NotNull(assetLoader, nameof(assetLoader)); Guard.NotNull(assetFolderRepository, nameof(assetFolderRepository)); + Guard.NotNull(options, nameof(options)); Guard.NotNull(queryParser, nameof(queryParser)); this.assetEnricher = assetEnricher; this.assetRepository = assetRepository; this.assetLoader = assetLoader; this.assetFolderRepository = assetFolderRepository; + this.options = options.Value; this.queryParser = queryParser; } @@ -53,7 +58,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries while (id != DomainId.Empty) { - var folder = await assetFolderRepository.FindAssetFolderAsync(appId, id, ct); + var folder = await FindFolderCoreAsync(appId, id, ct); if (folder == null || result.Any(x => x.Id == folder.Id)) { @@ -75,7 +80,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { using (Profiler.TraceMethod()) { - var assetFolders = await assetFolderRepository.QueryAsync(appId, parentId, ct); + var assetFolders = await QueryFoldersCoreAsync(appId, parentId, ct); return assetFolders; } @@ -86,7 +91,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { using (Profiler.TraceMethod()) { - var assetFolders = await assetFolderRepository.QueryAsync(context.App.Id, parentId, ct); + var assetFolders = await QueryFoldersCoreAsync(context, parentId, ct); return assetFolders; } @@ -99,7 +104,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries using (Profiler.TraceMethod()) { - var asset = await assetRepository.FindAssetByHashAsync(context.App.Id, hash, fileName, fileSize, ct); + var asset = await FindByHashCoreAsync(context, hash, fileName, fileSize, ct); if (asset == null) { @@ -117,7 +122,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries using (Profiler.TraceMethod()) { - var asset = await assetRepository.FindAssetBySlugAsync(context.App.Id, slug, ct); + var asset = await FindBySlugCoreAsync(context, slug, ct); if (asset == null) { @@ -135,7 +140,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries using (Profiler.TraceMethod()) { - var asset = await assetRepository.FindAssetAsync(id, ct); + var asset = await FindCoreAsync(id, ct); if (asset == null) { @@ -161,7 +166,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries } else { - asset = await assetRepository.FindAssetAsync(context.App.Id, id, ct); + asset = await FindCoreAsync(context, id, ct); } if (asset == null) @@ -187,7 +192,7 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries { q = await queryParser.ParseAsync(context, q); - var assets = await assetRepository.QueryAsync(context.App.Id, parentId, q, ct); + var assets = await QueryCoreAsync(context, parentId, q, ct); if (q.Ids != null && q.Ids.Count > 0) { @@ -222,5 +227,100 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries return await assetEnricher.EnrichAsync(assets, context, ct); } } + + private async Task> QueryFoldersCoreAsync(Context context, DomainId parentId, + CancellationToken ct) + { + using (var timeout = new CancellationTokenSource(options.TimeoutQuery)) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct)) + { + return await assetFolderRepository.QueryAsync(context.App.Id, parentId, combined.Token); + } + } + } + + private async Task> QueryFoldersCoreAsync(DomainId appId, DomainId parentId, + CancellationToken ct) + { + using (var timeout = new CancellationTokenSource(options.TimeoutQuery)) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct)) + { + return await assetFolderRepository.QueryAsync(appId, parentId, combined.Token); + } + } + } + + private async Task> QueryCoreAsync(Context context, DomainId? parentId, Q q, + CancellationToken ct) + { + using (var timeout = new CancellationTokenSource(options.TimeoutQuery)) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct)) + { + return await assetRepository.QueryAsync(context.App.Id, parentId, q, combined.Token); + } + } + } + + private async Task FindFolderCoreAsync(DomainId appId, DomainId id, CancellationToken ct) + { + using (var timeout = new CancellationTokenSource(options.TimeoutFind)) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct)) + { + return await assetFolderRepository.FindAssetFolderAsync(appId, id, combined.Token); + } + } + } + + private async Task FindByHashCoreAsync(Context context, string hash, string fileName, long fileSize, + CancellationToken ct) + { + using (var timeout = new CancellationTokenSource(options.TimeoutFind)) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct)) + { + return await assetRepository.FindAssetByHashAsync(context.App.Id, hash, fileName, fileSize, combined.Token); + } + } + } + + private async Task FindBySlugCoreAsync(Context context, string slug, + CancellationToken ct) + { + using (var timeout = new CancellationTokenSource(options.TimeoutFind)) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct)) + { + return await assetRepository.FindAssetBySlugAsync(context.App.Id, slug, combined.Token); + } + } + } + + private async Task FindCoreAsync(DomainId id, + CancellationToken ct) + { + using (var timeout = new CancellationTokenSource(options.TimeoutFind)) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct)) + { + return await assetRepository.FindAssetAsync(id, combined.Token); + } + } + } + + private async Task FindCoreAsync(Context context, DomainId id, + CancellationToken ct) + { + using (var timeout = new CancellationTokenSource(options.TimeoutFind)) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct)) + { + return await assetRepository.FindAssetAsync(context.App.Id, id, combined.Token); + } + } + } } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs index 354d247c9..4abed69cb 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/ContentOptions.cs @@ -5,6 +5,8 @@ // All rights reserved. Licensed under the MIT license. // ========================================================================== +using System; + namespace Squidex.Domain.Apps.Entities.Contents { public sealed class ContentOptions @@ -12,5 +14,9 @@ namespace Squidex.Domain.Apps.Entities.Contents public int DefaultPageSize { get; set; } = 200; public int MaxResults { get; set; } = 200; + + public TimeSpan TimeoutFind { get; set; } = TimeSpan.FromSeconds(1); + + public TimeSpan TimeoutQuery { get; set; } = TimeSpan.FromSeconds(5); } } diff --git a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs index 8b7e7c6e1..f907f516c 100644 --- a/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs +++ b/backend/src/Squidex.Domain.Apps.Entities/Contents/Queries/ContentQueryService.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Entities.Contents.Repositories; using Squidex.Domain.Apps.Entities.Schemas; using Squidex.Infrastructure; @@ -29,25 +30,28 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries private readonly IContentRepository contentRepository; private readonly IContentLoader contentLoader; private readonly ContentQueryParser queryParser; + private readonly ContentOptions options; public ContentQueryService( IAppProvider appProvider, IContentEnricher contentEnricher, IContentRepository contentRepository, IContentLoader contentLoader, + IOptions options, ContentQueryParser queryParser) { Guard.NotNull(appProvider, nameof(appProvider)); Guard.NotNull(contentEnricher, nameof(contentEnricher)); Guard.NotNull(contentRepository, nameof(contentRepository)); Guard.NotNull(contentLoader, nameof(contentLoader)); + Guard.NotNull(options, nameof(options)); Guard.NotNull(queryParser, nameof(queryParser)); this.appProvider = appProvider; this.contentEnricher = contentEnricher; this.contentRepository = contentRepository; this.contentLoader = contentLoader; - this.queryParser = queryParser; + this.options = options.Value; this.queryParser = queryParser; } @@ -68,7 +72,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries } else { - content = await contentRepository.FindContentAsync(context.App, schema, id, context.Scope(), ct); + content = await FindCoreAsync(context, id, schema, ct); } if (content == null || content.SchemaId.Id != schema.Id) @@ -101,7 +105,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries q = await queryParser.ParseAsync(context, q, schema); - var contents = await contentRepository.QueryAsync(context.App, schema, q, context.Scope(), ct); + var contents = await QueryCoreAsync(context, q, schema, ct); if (q.Ids != null && q.Ids.Count > 0) { @@ -133,7 +137,7 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries q = await queryParser.ParseAsync(context, q); - var contents = await contentRepository.QueryAsync(context.App, schemas, q, context.Scope(), ct); + var contents = await QueryCoreAsync(context, q, schemas, ct); if (q.Ids != null && q.Ids.Count > 0) { @@ -217,6 +221,42 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries return schemas.Where(x => IsAccessible(x) && HasPermission(context, x, Permissions.AppContentsReadOwn)).ToList(); } + private async Task> QueryCoreAsync(Context context, Q q, ISchemaEntity schema, + CancellationToken ct) + { + using (var timeout = new CancellationTokenSource(options.TimeoutQuery)) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct)) + { + return await contentRepository.QueryAsync(context.App, schema, q, context.Scope(), ct); + } + } + } + + private async Task> QueryCoreAsync(Context context, Q q, List schemas, + CancellationToken ct) + { + using (var timeout = new CancellationTokenSource(options.TimeoutQuery)) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct)) + { + return await contentRepository.QueryAsync(context.App, schemas, q, context.Scope(), ct); + } + } + } + + private async Task FindCoreAsync(Context context, DomainId id, ISchemaEntity schema, + CancellationToken ct) + { + using (var timeout = new CancellationTokenSource(options.TimeoutFind)) + { + using (var combined = CancellationTokenSource.CreateLinkedTokenSource(timeout.Token, ct)) + { + return await contentRepository.FindContentAsync(context.App, schema, id, context.Scope(), combined.Token); + } + } + } + private static bool IsAccessible(ISchemaEntity schema) { return schema.SchemaDef.IsPublished; diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs index d786b0188..802725fa2 100644 --- a/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs +++ b/backend/src/Squidex/Areas/IdentityServer/Config/IdentityServerServices.cs @@ -55,6 +55,9 @@ namespace Squidex.Areas.IdentityServer.Config services.AddSingletonAs() .As(); + services.AddSingletonAs() + .AsSelf(); + services.AddSingletonAs() .AsSelf(); diff --git a/backend/src/Squidex/Areas/IdentityServer/Config/TokenInitializer.cs b/backend/src/Squidex/Areas/IdentityServer/Config/TokenInitializer.cs new file mode 100644 index 000000000..03681d100 --- /dev/null +++ b/backend/src/Squidex/Areas/IdentityServer/Config/TokenInitializer.cs @@ -0,0 +1,82 @@ +// ========================================================================== +// 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 Microsoft.Extensions.Options; +using MongoDB.Driver; +using OpenIddict.Abstractions; +using OpenIddict.MongoDb; +using OpenIddict.MongoDb.Models; +using Squidex.Hosting; +using Squidex.Infrastructure.Timers; + +namespace Squidex.Areas.IdentityServer.Config +{ + public sealed class TokenStoreInitializer : IInitializable + { + private readonly OpenIddictMongoDbOptions options; + private readonly IServiceProvider serviceProvider; + private CompletionTimer timer; + + public TokenStoreInitializer(IOptions options, + IServiceProvider serviceProvider) + { + this.options = options.Value; + + this.serviceProvider = serviceProvider; + } + + public async Task InitializeAsync(CancellationToken ct) + { + await SetupIndexAsync(ct); + + await PruneAsync(ct); + + timer = new CompletionTimer((int)TimeSpan.FromHours(6).TotalMilliseconds, async ct => + { + await PruneAsync(ct); + }); + } + + private async Task PruneAsync(CancellationToken ct) + { + using (var scope = serviceProvider.CreateScope()) + { + var tokenManager = scope.ServiceProvider.GetRequiredService(); + + await tokenManager.PruneAsync(DateTimeOffset.UtcNow.AddDays(-40), ct); + } + } + + private async Task SetupIndexAsync(CancellationToken ct) + { + using (var scope = serviceProvider.CreateScope()) + { + var database = await scope.ServiceProvider.GetRequiredService().GetDatabaseAsync(ct); + + var collection = database.GetCollection>(options.TokensCollectionName); + + await collection.Indexes.CreateOneAsync( + new CreateIndexModel>( + Builders>.IndexKeys + .Ascending(x => x.ReferenceId)), + cancellationToken: ct); + } + } + + public async Task ReleaseAsync(CancellationToken ct) + { + if (timer != null) + { + await timer.StopAsync(); + } + } + } +} diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index 9150ad045..7cba5a000 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -274,7 +274,17 @@ * * Warning: Use pagination and not large number of items. */ - "maxResults": 200 + "maxResults": 200, + + /* + * The timeout when searching for single items in the database. + */ + "timeoutFind": "00:00:01", + + /* + * The timeout when searching for multiple items in the database. + */ + "timeoutQuery": "00:00:05" }, "assets": { @@ -303,7 +313,17 @@ /* * True to delete assets files permanently. */ - "deletePermanent": false + "deletePermanent": false, + + /* + * The timeout when searching for single items in the database. + */ + "timeoutFind": "00:00:01", + + /* + * The timeout when searching for multiple items in the database. + */ + "timeoutQuery": "00:00:05" }, "logging": { diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs index 0a3ce49ec..eb44a8808 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Assets/Queries/AssetQueryServiceTests.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using FakeItEasy; +using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Entities.Assets.Repositories; using Squidex.Domain.Apps.Entities.TestHelpers; using Squidex.Infrastructure; @@ -38,7 +39,15 @@ namespace Squidex.Domain.Apps.Entities.Assets.Queries A.CallTo(() => queryParser.ParseAsync(requestContext, A._)) .ReturnsLazily(c => Task.FromResult(c.GetArgument(1)!)); - sut = new AssetQueryService(assetEnricher, assetRepository, assetLoader, assetFolderRepository, queryParser); + var options = Options.Create(new AssetOptions()); + + sut = new AssetQueryService( + assetEnricher, + assetRepository, + assetLoader, + assetFolderRepository, + options, + queryParser); } [Fact] diff --git a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs index e20aee9d3..9b6558391 100644 --- a/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs +++ b/backend/tests/Squidex.Domain.Apps.Entities.Tests/Contents/Queries/ContentQueryServiceTests.cs @@ -11,6 +11,7 @@ using System.Security.Claims; using System.Threading; using System.Threading.Tasks; using FakeItEasy; +using Microsoft.Extensions.Options; using Squidex.Domain.Apps.Core.Contents; using Squidex.Domain.Apps.Core.Schemas; using Squidex.Domain.Apps.Entities.Contents.Repositories; @@ -58,11 +59,14 @@ namespace Squidex.Domain.Apps.Entities.Contents.Queries A.CallTo(() => queryParser.ParseAsync(A._, A._, A._)) .ReturnsLazily(c => Task.FromResult(c.GetArgument(1)!)); + var options = Options.Create(new ContentOptions()); + sut = new ContentQueryService( appProvider, contentEnricher, contentRepository, contentVersionLoader, + options, queryParser); }