Skip to content

Commit 02ccacd

Browse files
Fixed executor swap logic when using the proxy . (#8538)
Co-authored-by: Glen <[email protected]>
1 parent 78a8825 commit 02ccacd

File tree

6 files changed

+67
-43
lines changed

6 files changed

+67
-43
lines changed

src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HttpRequestExecutorProxy.cs

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,16 +22,10 @@ public async ValueTask<ExecutorSession> GetOrCreateSessionAsync(CancellationToke
2222
return executor.Features.GetRequired<ExecutorSession>();
2323
}
2424

25-
protected override void OnRequestExecutorUpdated(IRequestExecutor? executor)
25+
protected override void OnConfigureRequestExecutor(IRequestExecutor newExecutor, IRequestExecutor? oldExecutor)
2626
{
27-
if (executor is null)
28-
{
29-
_session = null;
30-
return;
31-
}
32-
33-
var session = new ExecutorSession(executor);
34-
executor.Features.Set(session);
27+
var session = new ExecutorSession(newExecutor);
28+
newExecutor.Features.Set(session);
3529
_session = session;
3630
}
3731

src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EvictSchemaTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ public async Task Evict_Default_Schema()
2828
}));
2929

3030
// act
31+
await Task.Delay(1000, cts.Token);
3132
await server.GetAsync(new ClientQueryRequest { Query = "{ evict }" });
33+
await Task.Delay(2000, cts.Token);
3234
newExecutorCreatedResetEvent.Wait(cts.Token);
3335

3436
// assert
@@ -58,9 +60,11 @@ public async Task Evict_Named_Schema()
5860
}));
5961

6062
// act
63+
await Task.Delay(1000, cts.Token);
6164
await server.GetAsync(
6265
new ClientQueryRequest { Query = "{ evict }" },
6366
"/evict");
67+
await Task.Delay(2000, cts.Token);
6468
newExecutorCreatedResetEvent.Wait(cts.Token);
6569

6670
// assert

src/HotChocolate/Core/src/Execution.Abstractions/Execution/RequestExecutorProxy.cs

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
using System.Threading.Channels;
12
using HotChocolate.Features;
3+
using HotChocolate.Utilities;
24

35
namespace HotChocolate.Execution;
46

@@ -14,6 +16,15 @@ public class RequestExecutorProxy : IRequestExecutor, IDisposable
1416
private readonly IRequestExecutorProvider _executorProvider;
1517
private readonly string _schemaName;
1618
private readonly IDisposable? _eventSubscription;
19+
private readonly CancellationTokenSource _cts = new();
20+
private readonly Channel<RequestExecutorEvent> _events =
21+
Channel.CreateBounded<RequestExecutorEvent>(
22+
new BoundedChannelOptions(1)
23+
{
24+
SingleReader = true,
25+
SingleWriter = true,
26+
FullMode = BoundedChannelFullMode.DropOldest
27+
});
1728
private bool _disposed;
1829

1930
/// <summary>
@@ -42,6 +53,7 @@ public RequestExecutorProxy(
4253

4354
var observer = new RequestExecutorEventObserver(OnRequestExecutorEvent);
4455
_eventSubscription = executorEvents.Subscribe(observer);
56+
OnUpdateRequestExecutorAsync(_cts.Token).FireAndForget();
4557
}
4658

4759
/// <summary>
@@ -178,7 +190,7 @@ public async ValueTask<IRequestExecutor> GetExecutorAsync(
178190
.ConfigureAwait(false);
179191

180192
CurrentExecutor = executor;
181-
OnRequestExecutorUpdated(executor);
193+
OnConfigureRequestExecutor(executor, null);
182194
}
183195
else
184196
{
@@ -193,7 +205,11 @@ public async ValueTask<IRequestExecutor> GetExecutorAsync(
193205
return executor;
194206
}
195207

196-
protected virtual void OnRequestExecutorUpdated(IRequestExecutor? executor)
208+
protected virtual void OnConfigureRequestExecutor(IRequestExecutor newExecutor, IRequestExecutor? oldExecutor)
209+
{
210+
}
211+
212+
protected virtual void OnAfterRequestExecutorSwapped(IRequestExecutor newExecutor, IRequestExecutor oldExecutor)
197213
{
198214
}
199215

@@ -204,33 +220,42 @@ private void OnRequestExecutorEvent(RequestExecutorEvent eventArgs)
204220
return;
205221
}
206222

207-
if (eventArgs.Type is RequestExecutorEventType.Evicted)
223+
if (eventArgs.Type is RequestExecutorEventType.Created)
208224
{
209-
_semaphore.Wait();
210-
211-
try
212-
{
213-
CurrentExecutor = null;
214-
OnRequestExecutorUpdated(null);
215-
}
216-
finally
217-
{
218-
_semaphore.Release();
219-
}
225+
_events.Writer.TryWrite(eventArgs);
220226
}
221-
else if (eventArgs.Type is RequestExecutorEventType.Created)
227+
}
228+
229+
private async Task OnUpdateRequestExecutorAsync(CancellationToken ct)
230+
{
231+
await foreach (var eventArgs in _events.Reader.ReadAllAsync(ct).ConfigureAwait(false))
222232
{
223-
_semaphore.Wait();
233+
IRequestExecutor newExecutor;
234+
IRequestExecutor oldExecutor;
235+
236+
await _semaphore.WaitAsync(ct);
224237

225238
try
226239
{
227-
CurrentExecutor = eventArgs.Executor;
228-
OnRequestExecutorUpdated(eventArgs.Executor);
240+
// events are only raised when there is an initial executor
241+
// so its guaranteed that we have a CurrentExecutor at this point.
242+
oldExecutor = CurrentExecutor!;
243+
newExecutor = eventArgs.Executor;
244+
245+
OnConfigureRequestExecutor(newExecutor, oldExecutor);
246+
247+
// we only assign the executor after we have run configuration logic
248+
// on the new instance.
249+
CurrentExecutor = newExecutor;
229250
}
230251
finally
231252
{
232253
_semaphore.Release();
233254
}
255+
256+
// after the swap of the executors we allow classes extending this proxy
257+
// to run notification logic that does not run within the lock context.
258+
OnAfterRequestExecutorSwapped(newExecutor, oldExecutor);
234259
}
235260
}
236261

@@ -239,8 +264,17 @@ public void Dispose()
239264
if (!_disposed)
240265
{
241266
CurrentExecutor = null;
267+
_cts.Cancel();
268+
_cts.Dispose();
269+
_events.Writer.TryComplete();
242270
_eventSubscription?.Dispose();
243271
_semaphore.Dispose();
272+
273+
while (_events.Reader.TryRead(out _))
274+
{
275+
// we drain the channel
276+
}
277+
244278
_disposed = true;
245279
}
246280
}

src/HotChocolate/Core/src/Execution.Abstractions/HotChocolate.Execution.Abstractions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<ProjectReference Include="..\..\..\Language\src\Language.SyntaxTree\HotChocolate.Language.SyntaxTree.csproj" />
2424
<ProjectReference Include="..\..\..\Language\src\Language.Web\HotChocolate.Language.Web.csproj" />
2525
<ProjectReference Include="..\..\..\Primitives\src\Primitives\HotChocolate.Primitives.csproj" />
26+
<ProjectReference Include="..\..\..\Utilities\src\Utilities.Tasks\HotChocolate.Utilities.Tasks.csproj" />
2627
<ProjectReference Include="..\Features\HotChocolate.Features.csproj" />
2728
<ProjectReference Include="..\Types.Abstractions\HotChocolate.Types.Abstractions.csproj" />
2829
</ItemGroup>

src/HotChocolate/Core/src/Execution/RequestExecutorManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ private async Task UpdateRequestExecutorAsync(string schemaName, RegisteredExecu
226226
previousExecutor.TypeModuleChangeMonitor.Dispose();
227227

228228
// This will hot swap the request executor.
229-
await CreateRequestExecutorAsync(schemaName, false, CancellationToken.None)
229+
await CreateRequestExecutorAsync(schemaName, isInitialCreation: false, CancellationToken.None)
230230
.ConfigureAwait(false);
231231

232232
previousExecutor.DiagnosticEvents.ExecutorEvicted(schemaName, previousExecutor.Executor);

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

Lines changed: 6 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,14 @@ public async Task Ensure_Executor_Is_Correctly_Swapped_When_Evicted()
4141
.Services
4242
.BuildServiceProvider()
4343
.GetRequiredService<RequestExecutorManager>();
44-
var evicted = false;
4544
var updated = false;
4645

4746
var proxy = new TestProxy(resolver, resolver, ISchemaDefinition.DefaultName);
48-
proxy.ExecutorEvicted += () =>
47+
proxy.ExecutorUpdated += () =>
4948
{
50-
evicted = true;
49+
updated = true;
5150
executorUpdatedResetEvent.Set();
5251
};
53-
proxy.ExecutorUpdated += () => updated = true;
5452

5553
// act
5654
var a = await proxy.GetExecutorAsync(CancellationToken.None);
@@ -60,7 +58,6 @@ public async Task Ensure_Executor_Is_Correctly_Swapped_When_Evicted()
6058

6159
// assert
6260
Assert.NotSame(a, b);
63-
Assert.True(evicted);
6461
Assert.True(updated);
6562
}
6663

@@ -70,19 +67,13 @@ private class TestProxy(
7067
string schemaName)
7168
: RequestExecutorProxy(executorProvider, executorEvents, schemaName)
7269
{
73-
public event Action? ExecutorEvicted;
7470
public event Action? ExecutorUpdated;
7571

76-
protected override void OnRequestExecutorUpdated(IRequestExecutor? executor)
72+
protected override void OnAfterRequestExecutorSwapped(
73+
IRequestExecutor newExecutor,
74+
IRequestExecutor oldExecutor)
7775
{
78-
if (executor is null)
79-
{
80-
ExecutorEvicted?.Invoke();
81-
}
82-
else
83-
{
84-
ExecutorUpdated?.Invoke();
85-
}
76+
ExecutorUpdated?.Invoke();
8677
}
8778
}
8879
}

0 commit comments

Comments
 (0)