diff --git a/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs b/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs index 430b1340c..abc476c8a 100644 --- a/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs +++ b/backend/src/Squidex.Web/Pipeline/ApiCostsFilter.cs @@ -70,6 +70,8 @@ namespace Squidex.Web.Pipeline } } } + + context.HttpContext.Response.Headers.Add("X-Costs", FilterDefinition.Costs.ToString()); } await next(); diff --git a/backend/src/Squidex.Web/Pipeline/CachingFilter.cs b/backend/src/Squidex.Web/Pipeline/CachingFilter.cs index 4e7e101ce..2acef292e 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingFilter.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingFilter.cs @@ -68,7 +68,7 @@ namespace Squidex.Web.Pipeline } } - cachingManager.Finish(httpContext, cachingOptions.MaxSurrogateKeys); + cachingManager.Finish(httpContext, cachingOptions.MaxSurrogateKeysSize); } } } diff --git a/backend/src/Squidex.Web/Pipeline/CachingManager.cs b/backend/src/Squidex.Web/Pipeline/CachingManager.cs index e213f8ddd..d260c7a1c 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingManager.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingManager.cs @@ -12,6 +12,7 @@ using System.Security.Cryptography; using System.Text; using System.Threading; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Squidex.Infrastructure; @@ -26,6 +27,9 @@ namespace Squidex.Web.Pipeline internal sealed class CacheContext : IRequestCache, IDisposable { + private static readonly ObjectPool StringBuilderPool = + new DefaultObjectPool(new StringBuilderPooledObjectPolicy()); + private readonly IncrementalHash hasher = IncrementalHash.CreateHash(HashAlgorithmName.SHA256); private readonly HashSet keys = new HashSet(); private readonly HashSet headers = new HashSet(); @@ -83,7 +87,7 @@ namespace Squidex.Web.Pipeline } } - public void Finish(HttpResponse response, int maxSurrogateKeys) + public void Finish(HttpResponse response, int maxSurrogateKeySize) { if (hasDependency && !response.Headers.ContainsKey(HeaderNames.ETag)) { @@ -96,11 +100,44 @@ namespace Squidex.Web.Pipeline } } - if (keys.Count <= maxSurrogateKeys) + if (keys.Count > 0 && maxSurrogateKeySize > 0) { - var value = string.Join(" ", keys); + const int GuidLength = 36; + + var stringBuilder = StringBuilderPool.Get(); + try + { + foreach (var key in keys) + { + if (stringBuilder.Length == 0) + { + if (stringBuilder.Length + GuidLength > maxSurrogateKeySize) + { + break; + } + } + else + { + if (stringBuilder.Length + GuidLength + 1 > maxSurrogateKeySize) + { + break; + } + + stringBuilder.Append(' '); + } + + stringBuilder.Append(key); + } - response.Headers.Add("Surrogate-Key", value); + if (stringBuilder.Length > 0) + { + response.Headers.Add("Surrogate-Key", stringBuilder.ToString()); + } + } + finally + { + StringBuilderPool.Return(stringBuilder); + } } if (headers.Count > 0) @@ -180,7 +217,7 @@ namespace Squidex.Web.Pipeline } } - public void Finish(HttpContext httpContext, int maxSurrogateKeys) + public void Finish(HttpContext httpContext, int maxSurrogateKeySize) { Guard.NotNull(httpContext); @@ -188,7 +225,7 @@ namespace Squidex.Web.Pipeline if (cacheContext != null) { - cacheContext.Finish(httpContext.Response, maxSurrogateKeys); + cacheContext.Finish(httpContext.Response, maxSurrogateKeySize); } } } diff --git a/backend/src/Squidex.Web/Pipeline/CachingOptions.cs b/backend/src/Squidex.Web/Pipeline/CachingOptions.cs index f89431cab..21d68dbcb 100644 --- a/backend/src/Squidex.Web/Pipeline/CachingOptions.cs +++ b/backend/src/Squidex.Web/Pipeline/CachingOptions.cs @@ -11,6 +11,6 @@ namespace Squidex.Web.Pipeline { public bool StrongETag { get; set; } - public int MaxSurrogateKeys { get; set; } = 200; + public int MaxSurrogateKeysSize { get; set; } = 17000; } } diff --git a/backend/src/Squidex/appsettings.json b/backend/src/Squidex/appsettings.json index be7bb65d5..fc1493c6a 100644 --- a/backend/src/Squidex/appsettings.json +++ b/backend/src/Squidex/appsettings.json @@ -37,9 +37,9 @@ "strongETag": false, /* - * Restrict the surrogate keys to results that have less than 200 items. + * Restrict the surrogate keys to 17KB. */ - "maxItemsForSurrogateKeys": 200 + "maxSurrogateKeysSize": 17000 }, "languages": { diff --git a/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs b/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs index c281cce99..344999256 100644 --- a/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs +++ b/backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs @@ -215,7 +215,7 @@ namespace Squidex.Web.Pipeline var id1 = Guid.NewGuid(); var id2 = Guid.NewGuid(); - cachingOptions.MaxSurrogateKeys = 2; + cachingOptions.MaxSurrogateKeysSize = 100; await sut.OnActionExecutionAsync(executingContext, () => { @@ -228,13 +228,32 @@ namespace Squidex.Web.Pipeline Assert.Equal($"{id1} {id2}", httpContext.Response.Headers["Surrogate-Key"]); } + [Fact] + public async Task Should_append_surrogate_keys_if_just_enough_space_for_one() + { + var id1 = Guid.NewGuid(); + var id2 = Guid.NewGuid(); + + cachingOptions.MaxSurrogateKeysSize = 36; + + await sut.OnActionExecutionAsync(executingContext, () => + { + cachingManager.AddDependency(id1, 12); + cachingManager.AddDependency(id2, 12); + + return Task.FromResult(executedContext); + }); + + Assert.Equal($"{id1}", httpContext.Response.Headers["Surrogate-Key"]); + } + [Fact] public async Task Should_not_append_surrogate_keys_if_maximum_is_exceeded() { var id1 = Guid.NewGuid(); var id2 = Guid.NewGuid(); - cachingOptions.MaxSurrogateKeys = 1; + cachingOptions.MaxSurrogateKeysSize = 20; await sut.OnActionExecutionAsync(executingContext, () => {