Skip to content

Commit

Permalink
Implement warmup requests
Browse files Browse the repository at this point in the history
  • Loading branch information
tobias-tengler committed Dec 2, 2024
1 parent a60e261 commit 1910a98
Show file tree
Hide file tree
Showing 11 changed files with 167 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public static IRequestExecutorBuilder UseQueryCachePipeline(
.UseDocumentValidation()
.UseOperationCache()
.UseOperationResolver()
.UseSkipWarmupExecution()
.UseOperationVariableCoercion()
.UseOperationExecution();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,9 @@ public enum ExecutionResultKind
/// A subscription response stream.
/// </summary>
SubscriptionResult,

/// <summary>
/// A no-op result for warmup requests.
/// </summary>
WarmupResult,
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,11 @@ public static OperationRequestBuilder SetUser(
this OperationRequestBuilder builder,
ClaimsPrincipal claimsPrincipal)
=> builder.SetGlobalState(nameof(ClaimsPrincipal), claimsPrincipal);

/// <summary>
/// Marks this request as a warmup request that will bypass security measures and skip execution.
/// </summary>
public static OperationRequestBuilder MarkAsWarmupRequest(
this OperationRequestBuilder builder)
=> builder.SetGlobalState(WellKnownContextData.IsWarmupRequest, true);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace HotChocolate.Execution;

public sealed class WarmupExecutionResult : ExecutionResult
{
public override ExecutionResultKind Kind => ExecutionResultKind.WarmupResult;

public override IReadOnlyDictionary<string, object?>? ContextData => null;
}
Original file line number Diff line number Diff line change
Expand Up @@ -328,4 +328,9 @@ public static class WellKnownContextData
/// The key to access the compiled requirements.
/// </summary>
public const string FieldRequirements = "HotChocolate.Types.ObjectField.Requirements";

/// <summary>
/// The key to determine whether the request is a warmup request.
/// </summary>
public const string IsWarmupRequest = "HotChocolate.AspNetCore.Warmup.IsWarmupRequest";
}
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ public static IRequestExecutorBuilder UseOperationVariableCoercion(
this IRequestExecutorBuilder builder) =>
builder.UseRequest(OperationVariableCoercionMiddleware.Create());

public static IRequestExecutorBuilder UseSkipWarmupExecution(
this IRequestExecutorBuilder builder) =>
builder.UseRequest(SkipWarmupExecutionMiddleware.Create());

public static IRequestExecutorBuilder UseReadPersistedOperation(
this IRequestExecutorBuilder builder) =>
builder.UseRequest(ReadPersistedOperationMiddleware.Create());
Expand Down Expand Up @@ -191,6 +195,7 @@ public static IRequestExecutorBuilder UsePersistedOperationPipeline(
.UseDocumentValidation()
.UseOperationCache()
.UseOperationResolver()
.UseSkipWarmupExecution()
.UseOperationVariableCoercion()
.UseOperationExecution();
}
Expand All @@ -215,6 +220,7 @@ public static IRequestExecutorBuilder UseAutomaticPersistedOperationPipeline(
.UseDocumentValidation()
.UseOperationCache()
.UseOperationResolver()
.UseSkipWarmupExecution()
.UseOperationVariableCoercion()
.UseOperationExecution();
}
Expand All @@ -229,6 +235,7 @@ internal static void AddDefaultPipeline(this IList<RequestCoreMiddleware> pipeli
pipeline.Add(DocumentValidationMiddleware.Create());
pipeline.Add(OperationCacheMiddleware.Create());
pipeline.Add(OperationResolverMiddleware.Create());
pipeline.Add(SkipWarmupExecutionMiddleware.Create());
pipeline.Add(OperationVariableCoercionMiddleware.Create());
pipeline.Add(OperationExecutionMiddleware.Create());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace HotChocolate.Execution;

public static class WarmupRequestExecutorExtensions
{
public static bool IsWarmupRequest(this IRequestContext requestContext)
=> requestContext.ContextData.ContainsKey(WellKnownContextData.IsWarmupRequest);
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ private OnlyPersistedOperationsAllowedMiddleware(

public ValueTask InvokeAsync(IRequestContext context)
{
// if all operations are allowed we can skip this middleware.
if(!_options.OnlyAllowPersistedDocuments)
// if all operations are allowed or the request is a warmup request, we can skip this middleware.
if(!_options.OnlyAllowPersistedDocuments || context.IsWarmupRequest())
{
return _next(context);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace HotChocolate.Execution.Pipeline;

internal sealed class SkipWarmupExecutionMiddleware(RequestDelegate next)
{
public async ValueTask InvokeAsync(IRequestContext context)
{
if (context.IsWarmupRequest())
{
context.Result = new WarmupExecutionResult();
return;
}

await next(context).ConfigureAwait(false);
}

public static RequestCoreMiddleware Create()
=> (_, next) =>
{
var middleware = new SkipWarmupExecutionMiddleware(next);
return context => middleware.InvokeAsync(context);
};
}
100 changes: 100 additions & 0 deletions src/HotChocolate/Core/test/Execution.Tests/WarmupRequestTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
using HotChocolate.Execution.Caching;
using HotChocolate.Language;
using Microsoft.Extensions.DependencyInjection;
using Moq;

namespace HotChocolate.Execution;

public class WarmupRequestTests
{
[Fact]
public async Task Warmup_Request_Warms_Up_Caches()
{
// arrange
var executor = await new ServiceCollection()
.AddGraphQL()
.AddQueryType<Query>()
.BuildRequestExecutorAsync();

var documentId = "f614e9a2ed367399e87751d41ca09105";
var warmupRequest = OperationRequestBuilder.New()
.SetDocument("query test($name: String!) { greeting(name: $name) }")
.SetDocumentId(documentId)
.MarkAsWarmupRequest()
.Build();

var regularRequest = OperationRequestBuilder.New()
.SetDocumentId(documentId)
.SetVariableValues(new Dictionary<string, object?> { ["name"] = "Foo" })
.Build();

// act 1
var warmupResult = await executor.ExecuteAsync(warmupRequest);

// assert 1
Assert.IsType<WarmupExecutionResult>(warmupResult);

var provider = executor.Services.GetCombinedServices();
var documentCache = provider.GetRequiredService<IDocumentCache>();
var operationCache = provider.GetRequiredService<IPreparedOperationCache>();

Assert.True(documentCache.TryGetDocument(documentId, out _));
Assert.Equal(1, operationCache.Count);

// act 2
var regularResult = await executor.ExecuteAsync(regularRequest);
var regularOperationResult = regularResult.ExpectOperationResult();

// assert 2
Assert.Null(regularOperationResult.Errors);
Assert.NotNull(regularOperationResult.Data);
Assert.NotEmpty(regularOperationResult.Data);

Assert.True(documentCache.TryGetDocument(documentId, out _));
Assert.Equal(1, operationCache.Count);
}

[Fact]
public async Task Warmup_Request_Can_Skip_Persisted_Operation_Check()
{
// arrange
var executor = await new ServiceCollection()
.AddGraphQL()
.ConfigureSchemaServices(services =>
{
services.AddSingleton<IOperationDocumentStorage>(_ => new Mock<IOperationDocumentStorage>().Object);
})
.AddQueryType<Query>()
.ModifyRequestOptions(options =>
{
options.PersistedOperations.OnlyAllowPersistedDocuments = true;
})
.UsePersistedOperationPipeline()
.BuildRequestExecutorAsync();

var documentId = "f614e9a2ed367399e87751d41ca09105";
var warmupRequest = OperationRequestBuilder.New()
.SetDocument("query test($name: String!) { greeting(name: $name) }")
.SetDocumentId(documentId)
.MarkAsWarmupRequest()
.Build();

// act
var warmupResult = await executor.ExecuteAsync(warmupRequest);

// assert
Assert.IsType<WarmupExecutionResult>(warmupResult);

var provider = executor.Services.GetCombinedServices();
var documentCache = provider.GetRequiredService<IDocumentCache>();
var operationCache = provider.GetRequiredService<IPreparedOperationCache>();

Assert.True(documentCache.TryGetDocument(documentId, out _));
Assert.Equal(1, operationCache.Count);
}

public class Query
{
public string Greeting(string name) => $"Hello {name}";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,7 @@ private static IRequestExecutorBuilder UseFusionDefaultPipeline(
.UseDocumentValidation()
.UseOperationCache()
.UseOperationResolver()
.UseSkipWarmupExecution()
.UseOperationVariableCoercion()
.UseDistributedOperationExecution();
}
Expand All @@ -588,6 +589,7 @@ private static IRequestExecutorBuilder UseFusionPersistedOperationPipeline(
.UseDocumentValidation()
.UseOperationCache()
.UseOperationResolver()
.UseSkipWarmupExecution()
.UseOperationVariableCoercion()
.UseDistributedOperationExecution();
}
Expand All @@ -612,6 +614,7 @@ private static IRequestExecutorBuilder UseFusionAutomaticPersistedOperationPipel
.UseDocumentValidation()
.UseOperationCache()
.UseOperationResolver()
.UseSkipWarmupExecution()
.UseOperationVariableCoercion()
.UseDistributedOperationExecution();
}
Expand Down

0 comments on commit 1910a98

Please sign in to comment.