Skip to content

Commit ae85bcf

Browse files
Merge d4a3cdd into cc2f3fe
2 parents cc2f3fe + d4a3cdd commit ae85bcf

File tree

10 files changed

+204
-62
lines changed

10 files changed

+204
-62
lines changed

src/HotChocolate/AspNetCore/src/AspNetCore/Warmup/RequestExecutorWarmupService.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
namespace HotChocolate.AspNetCore.Warmup;
66

77
internal sealed class RequestExecutorWarmupService(
8-
IOptionsMonitor<RequestExecutorSetup> optionsMonitor,
8+
IRequestExecutorOptionsMonitor optionsMonitor,
99
IRequestExecutorProvider provider) : IHostedService
1010
{
1111
public async Task StartAsync(CancellationToken cancellationToken)

src/HotChocolate/Core/src/Execution/Configuration/ConfigureRequestExecutorSetup.cs

Lines changed: 0 additions & 41 deletions
This file was deleted.
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using Microsoft.Extensions.Options;
2+
3+
namespace HotChocolate.Execution.Configuration;
4+
5+
internal sealed class DefaultRequestExecutorOptionsMonitor(IOptionsMonitor<RequestExecutorSetup> optionsMonitor)
6+
: IRequestExecutorOptionsMonitor
7+
{
8+
public RequestExecutorSetup Get(string schemaName) => optionsMonitor.Get(schemaName);
9+
10+
public IDisposable OnChange(Action<string> listener) => NoOpListener.Instance;
11+
12+
private sealed class NoOpListener : IDisposable
13+
{
14+
public void Dispose()
15+
{
16+
}
17+
18+
public static NoOpListener Instance { get; } = new();
19+
}
20+
}

src/HotChocolate/Core/src/Execution/Configuration/IConfigureRequestExecutorSetup.cs

Lines changed: 0 additions & 14 deletions
This file was deleted.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
namespace HotChocolate.Execution.Configuration;
2+
3+
/// <summary>
4+
/// Used for notifications when <see cref="RequestExecutorSetup"/> instances change.
5+
/// </summary>
6+
public interface IRequestExecutorOptionsMonitor
7+
{
8+
/// <summary>
9+
/// Returns a configured <see cref="RequestExecutorSetup"/>
10+
/// instance with the given name.
11+
/// </summary>
12+
RequestExecutorSetup Get(string schemaName);
13+
14+
/// <summary>
15+
/// Registers a listener to be called whenever a named
16+
/// <see cref="RequestExecutorSetup"/> changes.
17+
/// </summary>
18+
/// <param name="listener">
19+
/// The action to be invoked when <see cref="RequestExecutorSetup"/> has changed.
20+
/// </param>
21+
/// <returns>
22+
/// An <see cref="IDisposable"/> which should be disposed to stop listening for changes.
23+
/// </returns>
24+
IDisposable OnChange(Action<string> listener);
25+
}

src/HotChocolate/Core/src/Execution/DependencyInjection/InternalServiceCollectionExtensions.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,33 @@
11
using GreenDonut;
22
using GreenDonut.DependencyInjection;
33
using HotChocolate.Execution;
4+
using HotChocolate.Execution.Configuration;
45
using HotChocolate.Execution.DependencyInjection;
56
using HotChocolate.Execution.Options;
67
using HotChocolate.Execution.Processing;
78
using HotChocolate.Execution.Processing.Tasks;
89
using HotChocolate.Fetching;
910
using HotChocolate.Internal;
10-
using HotChocolate.Language;
1111
using HotChocolate.Types;
1212
using HotChocolate.Utilities;
1313
using Microsoft.Extensions.DependencyInjection.Extensions;
1414
using Microsoft.Extensions.ObjectPool;
15+
using Microsoft.Extensions.Options;
1516

1617
// ReSharper disable once CheckNamespace
1718
namespace Microsoft.Extensions.DependencyInjection;
1819

1920
internal static class InternalServiceCollectionExtensions
2021
{
22+
internal static IServiceCollection TryAddRequestExecutorOptionsMonitor(
23+
this IServiceCollection services)
24+
{
25+
services.TryAddSingleton<IRequestExecutorOptionsMonitor>(
26+
sp => new DefaultRequestExecutorOptionsMonitor(
27+
sp.GetRequiredService<IOptionsMonitor<RequestExecutorSetup>>()));
28+
return services;
29+
}
30+
2131
internal static IServiceCollection TryAddVariableCoercion(
2232
this IServiceCollection services)
2333
{

src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorBuilderExtensions.Caches.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
using HotChocolate.Execution.Configuration;
44
using HotChocolate.Language;
55
using Microsoft.Extensions.DependencyInjection.Extensions;
6-
using Microsoft.Extensions.Options;
76

87
namespace Microsoft.Extensions.DependencyInjection;
98

@@ -15,7 +14,7 @@ internal static IRequestExecutorBuilder AddDocumentCache(this IRequestExecutorBu
1514
builder.Name,
1615
static (sp, schemaName) =>
1716
{
18-
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<RequestExecutorSetup>>();
17+
var optionsMonitor = sp.GetRequiredService<IRequestExecutorOptionsMonitor>();
1918
var setup = optionsMonitor.Get((string)schemaName!);
2019
var options = setup.CreateSchemaOptions();
2120

src/HotChocolate/Core/src/Execution/DependencyInjection/RequestExecutorServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public static IServiceCollection AddGraphQLCore(this IServiceCollection services
4545

4646
// core services
4747
services
48+
.TryAddRequestExecutorOptionsMonitor()
4849
.TryAddTypeConverter()
4950
.TryAddInputFormatter()
5051
.TryAddInputParser()

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
using Microsoft.Extensions.DependencyInjection;
1919
using Microsoft.Extensions.DependencyInjection.Extensions;
2020
using Microsoft.Extensions.ObjectPool;
21-
using Microsoft.Extensions.Options;
2221
using static HotChocolate.Execution.ThrowHelper;
2322

2423
namespace HotChocolate.Execution;
@@ -31,15 +30,15 @@ internal sealed partial class RequestExecutorManager
3130
private readonly CancellationTokenSource _cts = new();
3231
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreBySchema = new();
3332
private readonly ConcurrentDictionary<string, RegisteredExecutor> _executors = new();
34-
private readonly IOptionsMonitor<RequestExecutorSetup> _optionsMonitor;
33+
private readonly IRequestExecutorOptionsMonitor _optionsMonitor;
3534
private readonly IServiceProvider _applicationServices;
3635
private readonly EventObservable _events = new();
3736
private readonly ChannelWriter<string> _executorEvictionChannelWriter;
3837
private ulong _version;
3938
private bool _disposed;
4039

4140
public RequestExecutorManager(
42-
IOptionsMonitor<RequestExecutorSetup> optionsMonitor,
41+
IRequestExecutorOptionsMonitor optionsMonitor,
4342
IServiceProvider serviceProvider)
4443
{
4544
ArgumentNullException.ThrowIfNull(optionsMonitor);
@@ -53,6 +52,8 @@ public RequestExecutorManager(
5352

5453
ConsumeExecutorEvictionsAsync(executorEvictionChannel.Reader, _cts.Token).FireAndForget();
5554

55+
_optionsMonitor.OnChange(EvictExecutor);
56+
5657
var schemaNames = _applicationServices.GetService<IEnumerable<SchemaName>>()?
5758
.Select(x => x.Value).Distinct().Order().ToImmutableArray();
5859
SchemaNames = schemaNames ?? [];
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
using HotChocolate.Types;
2+
using Microsoft.Extensions.DependencyInjection;
3+
4+
namespace HotChocolate.Execution.Configuration;
5+
6+
public class RequestExecutorOptionsMonitorTests
7+
{
8+
[Fact]
9+
public async Task RequestExecutorSetup_Can_Be_Provided_Through_RequestExecutorOptionsMonitor()
10+
{
11+
// arrange
12+
var monitor = new TestOptionsMonitor();
13+
monitor.Set(ISchemaDefinition.DefaultName, new RequestExecutorSetup
14+
{
15+
SchemaBuilder = SchemaBuilder.New()
16+
.AddQueryType(d => d.Field("field").Resolve("")),
17+
Pipeline = { new RequestMiddlewareConfiguration((_, _) => _ => ValueTask.CompletedTask) }
18+
});
19+
20+
var services = new ServiceCollection();
21+
services.AddSingleton<IRequestExecutorOptionsMonitor>(_ => monitor);
22+
23+
services.AddGraphQLServer();
24+
25+
// act
26+
var executor = await services.BuildServiceProvider().GetRequestExecutorAsync();
27+
28+
// assert
29+
executor.Schema.MatchInlineSnapshot(
30+
"""
31+
schema {
32+
query: ObjectType
33+
}
34+
35+
type ObjectType {
36+
field: String
37+
}
38+
""");
39+
}
40+
41+
[Fact]
42+
public async Task RequestExecutor_Can_Be_Reloaded_Through_RequestExecutorOptionsMonitor()
43+
{
44+
// arrange
45+
var executorEvictedResetEvent = new ManualResetEventSlim(false);
46+
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
47+
48+
var monitor = new TestOptionsMonitor();
49+
monitor.Set(ISchemaDefinition.DefaultName, new RequestExecutorSetup
50+
{
51+
SchemaBuilder = SchemaBuilder.New()
52+
.AddQueryType(d => d.Field("field").Resolve("")),
53+
Pipeline = { new RequestMiddlewareConfiguration((_, _) => _ => ValueTask.CompletedTask) }
54+
});
55+
56+
var services = new ServiceCollection();
57+
services.AddSingleton<IRequestExecutorOptionsMonitor>(_ => monitor);
58+
59+
services.AddGraphQLServer();
60+
61+
var manager = services.BuildServiceProvider().GetRequiredService<RequestExecutorManager>();
62+
63+
manager.Subscribe(new RequestExecutorEventObserver(@event =>
64+
{
65+
if (@event.Type == RequestExecutorEventType.Evicted)
66+
{
67+
executorEvictedResetEvent.Set();
68+
}
69+
}));
70+
71+
// act
72+
// assert
73+
var initialExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token);
74+
75+
monitor.Set(ISchemaDefinition.DefaultName, new RequestExecutorSetup
76+
{
77+
SchemaBuilder = SchemaBuilder.New()
78+
.AddQueryType(d => d.Field("field2").Resolve("")),
79+
Pipeline = { new RequestMiddlewareConfiguration((_, _) => _ => ValueTask.CompletedTask) }
80+
});
81+
82+
executorEvictedResetEvent.Wait(cts.Token);
83+
84+
var executorAfterEviction = await manager.GetExecutorAsync(cancellationToken: cts.Token);
85+
86+
Assert.NotSame(initialExecutor, executorAfterEviction);
87+
88+
executorAfterEviction.Schema.MatchInlineSnapshot(
89+
"""
90+
schema {
91+
query: ObjectType
92+
}
93+
94+
type ObjectType {
95+
field2: String
96+
}
97+
""");
98+
99+
cts.Dispose();
100+
}
101+
102+
private sealed class TestOptionsMonitor : IRequestExecutorOptionsMonitor
103+
{
104+
private readonly Dictionary<string, RequestExecutorSetup> _setups = new();
105+
private readonly List<Action<string>> _listeners = new();
106+
107+
public void Set(string schemaName, RequestExecutorSetup setup)
108+
{
109+
_setups[schemaName] = setup;
110+
111+
foreach (var listener in _listeners)
112+
{
113+
listener(schemaName);
114+
}
115+
}
116+
117+
public RequestExecutorSetup Get(string schemaName)
118+
{
119+
return _setups[schemaName];
120+
}
121+
122+
public IDisposable OnChange(Action<string> listener)
123+
{
124+
_listeners.Add(listener);
125+
return new Unsubscriber(this, listener);
126+
}
127+
128+
private void Unsubscribe(Action<string> listener)
129+
{
130+
_listeners.Remove(listener);
131+
}
132+
133+
private sealed class Unsubscriber(
134+
TestOptionsMonitor monitor,
135+
Action<string> listener)
136+
: IDisposable
137+
{
138+
public void Dispose() => monitor.Unsubscribe(listener);
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)