Browse Source

Introduce header to customize surrogate key and reduce default size.

pull/514/head
Sebastian 6 years ago
parent
commit
875925837e
  1. 56
      backend/src/Squidex.Web/Pipeline/CachingFilter.cs
  2. 36
      backend/src/Squidex.Web/Pipeline/CachingManager.cs
  3. 2
      backend/src/Squidex.Web/Pipeline/CachingOptions.cs
  4. 2
      backend/src/Squidex/appsettings.json
  5. 19
      backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs

56
backend/src/Squidex.Web/Pipeline/CachingFilter.cs

@ -33,42 +33,60 @@ namespace Squidex.Web.Pipeline
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{ {
var httpContext = context.HttpContext; cachingManager.Start(context.HttpContext);
cachingManager.Start(httpContext); AppendAuthHeaders(context.HttpContext);
cachingManager.AddHeader("Auth-State"); var resultContext = await next();
if (!string.IsNullOrWhiteSpace(httpContext.User.OpenIdSubject())) if (resultContext.HttpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag))
{ {
cachingManager.AddHeader(HeaderNames.Authorization); if (!cachingOptions.StrongETag && IsWeakEtag(etag))
}
else if (!string.IsNullOrWhiteSpace(httpContext.User.OpenIdClientId()))
{ {
cachingManager.AddHeader("Auth-ClientId"); etag = ToWeakEtag(etag);
}
var resultContext = await next(); resultContext.HttpContext.Response.Headers[HeaderNames.ETag] = etag;
}
if (httpContext.Response.Headers.TryGetString(HeaderNames.ETag, out var etag)) if (IsCacheable(resultContext.HttpContext, etag))
{ {
if (!cachingOptions.StrongETag && !etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase)) resultContext.Result = new StatusCodeResult(304);
{ }
etag = $"W/{etag}"; }
httpContext.Response.Headers[HeaderNames.ETag] = etag; cachingManager.Finish(resultContext.HttpContext);
} }
if (HttpMethods.IsGet(httpContext.Request.Method) && private static bool IsCacheable(HttpContext httpContext, string etag)
{
return HttpMethods.IsGet(httpContext.Request.Method) &&
httpContext.Response.StatusCode == 200 && httpContext.Response.StatusCode == 200 &&
httpContext.Request.Headers.TryGetString(HeaderNames.IfNoneMatch, out var noneMatch) && httpContext.Request.Headers.TryGetString(HeaderNames.IfNoneMatch, out var noneMatch) &&
string.Equals(etag, noneMatch, StringComparison.Ordinal)) string.Equals(etag, noneMatch, StringComparison.Ordinal);
}
private void AppendAuthHeaders(HttpContext httpContext)
{ {
resultContext.Result = new StatusCodeResult(304); cachingManager.AddHeader("Auth-State");
if (!string.IsNullOrWhiteSpace(httpContext.User.OpenIdSubject()))
{
cachingManager.AddHeader(HeaderNames.Authorization);
}
else if (!string.IsNullOrWhiteSpace(httpContext.User.OpenIdClientId()))
{
cachingManager.AddHeader("Auth-ClientId");
} }
} }
cachingManager.Finish(httpContext); private static string ToWeakEtag(string? etag)
{
return $"W/{etag}";
}
private static bool IsWeakEtag(string etag)
{
return !etag.StartsWith("W/", StringComparison.OrdinalIgnoreCase);
} }
} }
} }

36
backend/src/Squidex.Web/Pipeline/CachingManager.cs

@ -24,6 +24,8 @@ namespace Squidex.Web.Pipeline
{ {
public sealed class CachingManager : IRequestCache public sealed class CachingManager : IRequestCache
{ {
public const string SurrogateKeySizeHeader = "X-SurrogateKeys";
private const int MaxAllowedKeysSize = 20000;
private readonly ObjectPool<StringBuilder> stringBuilderPool; private readonly ObjectPool<StringBuilder> stringBuilderPool;
private readonly CachingOptions cachingOptions; private readonly CachingOptions cachingOptions;
private readonly IHttpContextAccessor httpContextAccessor; private readonly IHttpContextAccessor httpContextAccessor;
@ -34,8 +36,14 @@ namespace Squidex.Web.Pipeline
private readonly HashSet<string> keys = new HashSet<string>(); private readonly HashSet<string> keys = new HashSet<string>();
private readonly HashSet<string> headers = new HashSet<string>(); private readonly HashSet<string> headers = new HashSet<string>();
private readonly ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim(); private readonly ReaderWriterLockSlim slimLock = new ReaderWriterLockSlim();
private readonly int maxKeysSize;
private bool hasDependency; private bool hasDependency;
public CacheContext(int maxKeysSize)
{
this.maxKeysSize = maxKeysSize;
}
public void Dispose() public void Dispose()
{ {
hasher.Dispose(); hasher.Dispose();
@ -87,7 +95,7 @@ namespace Squidex.Web.Pipeline
} }
} }
public void Finish(HttpResponse response, int maxSurrogateKeySize, ObjectPool<StringBuilder> stringBuilderPool) public void Finish(HttpResponse response, ObjectPool<StringBuilder> stringBuilderPool)
{ {
if (hasDependency && !response.Headers.ContainsKey(HeaderNames.ETag)) if (hasDependency && !response.Headers.ContainsKey(HeaderNames.ETag))
{ {
@ -100,10 +108,8 @@ namespace Squidex.Web.Pipeline
} }
} }
if (keys.Count > 0 && maxSurrogateKeySize > 0) if (keys.Count > 0 && maxKeysSize > 0)
{ {
const int GuidLength = 36;
var stringBuilder = stringBuilderPool.Get(); var stringBuilder = stringBuilderPool.Get();
try try
{ {
@ -111,14 +117,14 @@ namespace Squidex.Web.Pipeline
{ {
if (stringBuilder.Length == 0) if (stringBuilder.Length == 0)
{ {
if (stringBuilder.Length + GuidLength > maxSurrogateKeySize) if (stringBuilder.Length + key.Length > maxKeysSize)
{ {
break; break;
} }
} }
else else
{ {
if (stringBuilder.Length + GuidLength + 1 > maxSurrogateKeySize) if (stringBuilder.Length + key.Length + 1 > maxKeysSize)
{ {
break; break;
} }
@ -183,7 +189,21 @@ namespace Squidex.Web.Pipeline
{ {
Guard.NotNull(httpContext); Guard.NotNull(httpContext);
httpContext.Features.Set(new CacheContext()); int maxKeysSize = GetKeysSize(httpContext);
httpContext.Features.Set(new CacheContext(maxKeysSize));
}
private int GetKeysSize(HttpContext httpContext)
{
var headers = httpContext.Request.Headers;
if (!headers.TryGetValue(SurrogateKeySizeHeader, out var header) || !int.TryParse(header, out int size))
{
size = cachingOptions.MaxSurrogateKeysSize;
}
return Math.Min(MaxAllowedKeysSize, size);
} }
public void AddDependency(Guid key, long version) public void AddDependency(Guid key, long version)
@ -233,7 +253,7 @@ namespace Squidex.Web.Pipeline
if (cacheContext != null) if (cacheContext != null)
{ {
cacheContext.Finish(httpContext.Response, cachingOptions.MaxSurrogateKeysSize, stringBuilderPool); cacheContext.Finish(httpContext.Response, stringBuilderPool);
} }
} }
} }

2
backend/src/Squidex.Web/Pipeline/CachingOptions.cs

@ -11,6 +11,6 @@ namespace Squidex.Web.Pipeline
{ {
public bool StrongETag { get; set; } public bool StrongETag { get; set; }
public int MaxSurrogateKeysSize { get; set; } = 17000; public int MaxSurrogateKeysSize { get; set; } = 8000;
} }
} }

2
backend/src/Squidex/appsettings.json

@ -39,7 +39,7 @@
/* /*
* Restrict the surrogate keys to 17KB. * Restrict the surrogate keys to 17KB.
*/ */
"maxSurrogateKeysSize": 17000 "maxSurrogateKeysSize": 8000
}, },
"languages": { "languages": {

19
backend/tests/Squidex.Web.Tests/Pipeline/CachingFilterTests.cs

@ -266,6 +266,25 @@ namespace Squidex.Web.Pipeline
Assert.Equal(StringValues.Empty, httpContext.Response.Headers["Surrogate-Key"]); Assert.Equal(StringValues.Empty, httpContext.Response.Headers["Surrogate-Key"]);
} }
[Fact]
public async Task Should_not_append_surrogate_keys_if_maximum_is_overriden()
{
var id1 = Guid.NewGuid();
var id2 = Guid.NewGuid();
httpContext.Request.Headers[CachingManager.SurrogateKeySizeHeader] = "20";
await sut.OnActionExecutionAsync(executingContext, () =>
{
cachingManager.AddDependency(id1, 12);
cachingManager.AddDependency(id2, 12);
return Task.FromResult(executedContext);
});
Assert.Equal(StringValues.Empty, httpContext.Response.Headers["Surrogate-Key"]);
}
[Fact] [Fact]
public async Task Should_generate_etag_from_ids_and_versions() public async Task Should_generate_etag_from_ids_and_versions()
{ {

Loading…
Cancel
Save