Skip to content

Commit 03463b0

Browse files
authored
Fixed issue with global state when using deferred execution and scoped services. (#7611)
1 parent 8be6806 commit 03463b0

File tree

7 files changed

+100
-47
lines changed

7 files changed

+100
-47
lines changed

src/HotChocolate/Core/src/Execution/IRequestContext.cs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,4 @@ public interface IRequestContext : IHasContextData
113113
/// Gets or sets an unexpected execution exception.
114114
/// </summary>
115115
Exception? Exception { get; set; }
116-
117-
/// <summary>
118-
/// Clones the request context.
119-
/// </summary>
120-
IRequestContext Clone();
121116
}

src/HotChocolate/Core/src/Execution/Pipeline/OperationExecutionMiddleware.cs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,8 @@ private async Task ExecuteOperationRequestAsync(
8989
{
9090
if (operation.Definition.Operation is OperationType.Subscription)
9191
{
92-
// since the request context is pooled we need to clone the context for
93-
// long-running executions.
94-
var cloned = context.Clone();
95-
9692
context.Result = await _subscriptionExecutor
97-
.ExecuteAsync(cloned, () => GetQueryRootValue(cloned))
93+
.ExecuteAsync(context, () => GetQueryRootValue(context))
9894
.ConfigureAwait(false);
9995
}
10096
else

src/HotChocolate/Core/src/Execution/RequestContext.cs

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -66,38 +66,6 @@ public DocumentValidatorResult? ValidationResult
6666

6767
public Exception? Exception { get; set; }
6868

69-
public IRequestContext Clone()
70-
{
71-
var cloned = new RequestContext(
72-
Schema,
73-
ExecutorVersion,
74-
ErrorHandler,
75-
DiagnosticEvents)
76-
{
77-
Request = Request,
78-
Services = Services,
79-
RequestAborted = RequestAborted,
80-
DocumentId = DocumentId,
81-
DocumentHash = DocumentHash,
82-
IsCachedDocument = IsCachedDocument,
83-
Document = Document,
84-
ValidationResult = ValidationResult,
85-
OperationId = OperationId,
86-
Operation = Operation,
87-
Variables = Variables,
88-
Result = Result,
89-
Exception = Exception,
90-
RequestIndex = RequestIndex,
91-
};
92-
93-
foreach (var item in _contextData)
94-
{
95-
cloned._contextData.TryAdd(item.Key, item.Value);
96-
}
97-
98-
return cloned;
99-
}
100-
10169
public void Initialize(IOperationRequest request, IServiceProvider services)
10270
{
10371
Request = request;
@@ -139,4 +107,3 @@ public void Reset()
139107
RequestIndex = default;
140108
}
141109
}
142-

src/HotChocolate/Core/src/Execution/RequestExecutor.cs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ internal async Task<IExecutionResult> ExecuteAsync(
103103
services.InitializeDataLoaderScope();
104104
}
105105

106-
RequestContext? context = _contextPool.Get();
106+
var context = _contextPool.Get();
107107

108108
try
109109
{
@@ -123,7 +123,15 @@ internal async Task<IExecutionResult> ExecuteAsync(
123123

124124
if (scope is null)
125125
{
126-
return context.Result;
126+
var localContext = context;
127+
128+
if (context.Result.IsStreamResult())
129+
{
130+
context.Result.RegisterForCleanup(() => _contextPool.Return(localContext));
131+
context = null;
132+
}
133+
134+
return localContext.Result;
127135
}
128136

129137
if (context.Result.IsStreamResult())

src/HotChocolate/Core/test/Execution.Tests/DeferAndStreamTestSchema.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,22 @@ public static async Task<IRequestExecutor> CreateAsync()
2121
.BuildRequestExecutorAsync();
2222
}
2323

24+
public static IServiceProvider CreateServiceProvider()
25+
{
26+
return new ServiceCollection()
27+
.AddGraphQL()
28+
.AddQueryType<Query>()
29+
.AddGlobalObjectIdentification()
30+
.ModifyOptions(
31+
o =>
32+
{
33+
o.EnableDefer = true;
34+
o.EnableStream = true;
35+
})
36+
.Services
37+
.BuildServiceProvider();
38+
}
39+
2440
public class Query
2541
{
2642
private readonly List<Person> _persons =

src/HotChocolate/Core/test/Execution.Tests/DeferTests.cs

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
using CookieCrumble;
2+
using HotChocolate.AspNetCore;
3+
using Microsoft.AspNetCore.Http;
4+
using Microsoft.Extensions.DependencyInjection;
25

36
namespace HotChocolate.Execution;
47

@@ -109,7 +112,11 @@ ... @defer(if: $defer) {
109112
}
110113
}
111114
""")
112-
.SetVariableValues(new Dictionary<string, object?> { { "defer", false }, })
115+
.SetVariableValues(
116+
new Dictionary<string, object?>
117+
{
118+
{ "defer", false },
119+
})
113120
.Build());
114121

115122
Assert.IsType<OperationResult>(result).MatchMarkdownSnapshot();
@@ -232,7 +239,11 @@ fragment Foo on Query {
232239
}
233240
}
234241
""")
235-
.SetVariableValues(new Dictionary<string, object?> { { "defer", false }, })
242+
.SetVariableValues(
243+
new Dictionary<string, object?>
244+
{
245+
{ "defer", false },
246+
})
236247
.Build());
237248

238249
Assert.IsType<OperationResult>(result).MatchMarkdownSnapshot();
@@ -301,6 +312,9 @@ ... @defer {
301312
[Fact]
302313
public async Task Ensure_GlobalState_Is_Passed_To_DeferContext_Single_Defer()
303314
{
315+
// this test ensures that the request context is not recycled until the
316+
// a stream is fully processed when no outer DI scope exists.
317+
304318
// arrange
305319
var executor = await DeferAndStreamTestSchema.CreateAsync();
306320

@@ -323,4 +337,49 @@ ... @defer {
323337

324338
Assert.IsType<ResponseStream>(result).MatchMarkdownSnapshot();
325339
}
340+
341+
[Fact]
342+
public async Task Ensure_GlobalState_Is_Passed_To_DeferContext_Single_Defer_2()
343+
{
344+
// this test ensures that the request context is not recycled until the
345+
// a stream is fully processed when an outer DI scope exists.
346+
347+
// arrange
348+
var services = DeferAndStreamTestSchema.CreateServiceProvider();
349+
var executor = await services.GetRequestExecutorAsync();
350+
await using var scope = services.CreateAsyncScope();
351+
352+
// act
353+
var result = await executor.ExecuteAsync(
354+
OperationRequestBuilder
355+
.New()
356+
.SetDocument(
357+
"""
358+
{
359+
... @defer {
360+
ensureState {
361+
state
362+
}
363+
}
364+
}
365+
""")
366+
.SetGlobalState("requestState", "state 123")
367+
.SetServices(scope.ServiceProvider)
368+
.Build());
369+
370+
Assert.IsType<ResponseStream>(result).MatchMarkdownSnapshot();
371+
}
372+
373+
private class StateRequestInterceptor : DefaultHttpRequestInterceptor
374+
{
375+
public override ValueTask OnCreateAsync(
376+
HttpContext context,
377+
IRequestExecutor requestExecutor,
378+
OperationRequestBuilder requestBuilder,
379+
CancellationToken cancellationToken)
380+
{
381+
requestBuilder.AddGlobalState("requestState", "bar");
382+
return base.OnCreateAsync(context, requestExecutor, requestBuilder, cancellationToken);
383+
}
384+
}
326385
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Ensure_GlobalState_Is_Passed_To_DeferContext_Single_Defer_2
2+
3+
```text
4+
{
5+
"data": {
6+
"ensureState": {
7+
"state": "state 123"
8+
}
9+
}
10+
}
11+
12+
```

0 commit comments

Comments
 (0)