Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace HotChocolate.AspNetCore.Warmup;

internal sealed class RequestExecutorWarmupService(
IOptionsMonitor<RequestExecutorSetup> optionsMonitor,
IRequestExecutorOptionsMonitor optionsMonitor,
IRequestExecutorProvider provider) : IHostedService
{
public async Task StartAsync(CancellationToken cancellationToken)
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Microsoft.Extensions.Options;

namespace HotChocolate.Execution.Configuration;

internal sealed class DefaultRequestExecutorOptionsMonitor(IOptionsMonitor<RequestExecutorSetup> optionsMonitor)
: IRequestExecutorOptionsMonitor
{
public RequestExecutorSetup Get(string schemaName) => optionsMonitor.Get(schemaName);

public IDisposable OnChange(Action<string> listener) => NoOpListener.Instance;

private sealed class NoOpListener : IDisposable
{
public void Dispose()
{
}

public static NoOpListener Instance { get; } = new();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
namespace HotChocolate.Execution.Configuration;

/// <summary>
/// Used for notifications when <see cref="RequestExecutorSetup"/> instances change.
/// </summary>
public interface IRequestExecutorOptionsMonitor
{
/// <summary>
/// Returns a configured <see cref="RequestExecutorSetup"/>
/// instance with the given name.
/// </summary>
RequestExecutorSetup Get(string schemaName);

/// <summary>
/// Registers a listener to be called whenever a named
/// <see cref="RequestExecutorSetup"/> changes.
/// </summary>
/// <param name="listener">
/// The action to be invoked when <see cref="RequestExecutorSetup"/> has changed.
/// </param>
/// <returns>
/// An <see cref="IDisposable"/> which should be disposed to stop listening for changes.
/// </returns>
IDisposable OnChange(Action<string> listener);
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,33 @@
using GreenDonut;
using GreenDonut.DependencyInjection;
using HotChocolate.Execution;
using HotChocolate.Execution.Configuration;
using HotChocolate.Execution.DependencyInjection;
using HotChocolate.Execution.Options;
using HotChocolate.Execution.Processing;
using HotChocolate.Execution.Processing.Tasks;
using HotChocolate.Fetching;
using HotChocolate.Internal;
using HotChocolate.Language;
using HotChocolate.Types;
using HotChocolate.Utilities;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;

// ReSharper disable once CheckNamespace
namespace Microsoft.Extensions.DependencyInjection;

internal static class InternalServiceCollectionExtensions
{
internal static IServiceCollection TryAddRequestExecutorOptionsMonitor(
this IServiceCollection services)
{
services.TryAddSingleton<IRequestExecutorOptionsMonitor>(
sp => new DefaultRequestExecutorOptionsMonitor(
sp.GetRequiredService<IOptionsMonitor<RequestExecutorSetup>>()));
return services;
}

internal static IServiceCollection TryAddVariableCoercion(
this IServiceCollection services)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
using HotChocolate.Execution.Configuration;
using HotChocolate.Language;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options;

namespace Microsoft.Extensions.DependencyInjection;

Expand All @@ -15,7 +14,7 @@ internal static IRequestExecutorBuilder AddDocumentCache(this IRequestExecutorBu
builder.Name,
static (sp, schemaName) =>
{
var optionsMonitor = sp.GetRequiredService<IOptionsMonitor<RequestExecutorSetup>>();
var optionsMonitor = sp.GetRequiredService<IRequestExecutorOptionsMonitor>();
var setup = optionsMonitor.Get((string)schemaName!);
var options = setup.CreateSchemaOptions();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ public static IServiceCollection AddGraphQLCore(this IServiceCollection services

// core services
services
.TryAddRequestExecutorOptionsMonitor()
.TryAddTypeConverter()
.TryAddInputFormatter()
.TryAddInputParser()
Expand Down
7 changes: 4 additions & 3 deletions src/HotChocolate/Core/src/Execution/RequestExecutorManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using static HotChocolate.Execution.ThrowHelper;

namespace HotChocolate.Execution;
Expand All @@ -31,15 +30,15 @@ internal sealed partial class RequestExecutorManager
private readonly CancellationTokenSource _cts = new();
private readonly ConcurrentDictionary<string, SemaphoreSlim> _semaphoreBySchema = new();
private readonly ConcurrentDictionary<string, RegisteredExecutor> _executors = new();
private readonly IOptionsMonitor<RequestExecutorSetup> _optionsMonitor;
private readonly IRequestExecutorOptionsMonitor _optionsMonitor;
private readonly IServiceProvider _applicationServices;
private readonly EventObservable _events = new();
private readonly ChannelWriter<string> _executorEvictionChannelWriter;
private ulong _version;
private bool _disposed;

public RequestExecutorManager(
IOptionsMonitor<RequestExecutorSetup> optionsMonitor,
IRequestExecutorOptionsMonitor optionsMonitor,
IServiceProvider serviceProvider)
{
ArgumentNullException.ThrowIfNull(optionsMonitor);
Expand All @@ -53,6 +52,8 @@ public RequestExecutorManager(

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

_optionsMonitor.OnChange(EvictExecutor);

var schemaNames = _applicationServices.GetService<IEnumerable<SchemaName>>()?
.Select(x => x.Value).Distinct().Order().ToImmutableArray();
SchemaNames = schemaNames ?? [];
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using HotChocolate.Types;
using Microsoft.Extensions.DependencyInjection;

namespace HotChocolate.Execution.Configuration;

public class RequestExecutorOptionsMonitorTests
{
[Fact]
public async Task RequestExecutorSetup_Can_Be_Provided_Through_RequestExecutorOptionsMonitor()
{
// arrange
var monitor = new TestOptionsMonitor();
monitor.Set(ISchemaDefinition.DefaultName, new RequestExecutorSetup
{
SchemaBuilder = SchemaBuilder.New()
.AddQueryType(d => d.Field("field").Resolve("")),
Pipeline = { new RequestMiddlewareConfiguration((_, _) => _ => ValueTask.CompletedTask) }
});

var services = new ServiceCollection();
services.AddSingleton<IRequestExecutorOptionsMonitor>(_ => monitor);

services.AddGraphQLServer();

// act
var executor = await services.BuildServiceProvider().GetRequestExecutorAsync();

// assert
executor.Schema.MatchInlineSnapshot(
"""
schema {
query: ObjectType
}
type ObjectType {
field: String
}
""");
}

[Fact]
public async Task RequestExecutor_Can_Be_Reloaded_Through_RequestExecutorOptionsMonitor()
{
// arrange
var executorEvictedResetEvent = new ManualResetEventSlim(false);
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

var monitor = new TestOptionsMonitor();
monitor.Set(ISchemaDefinition.DefaultName, new RequestExecutorSetup
{
SchemaBuilder = SchemaBuilder.New()
.AddQueryType(d => d.Field("field").Resolve("")),
Pipeline = { new RequestMiddlewareConfiguration((_, _) => _ => ValueTask.CompletedTask) }
});

var services = new ServiceCollection();
services.AddSingleton<IRequestExecutorOptionsMonitor>(_ => monitor);

services.AddGraphQLServer();

var manager = services.BuildServiceProvider().GetRequiredService<RequestExecutorManager>();

manager.Subscribe(new RequestExecutorEventObserver(@event =>
{
if (@event.Type == RequestExecutorEventType.Evicted)
{
executorEvictedResetEvent.Set();
}
}));

// act
// assert
var initialExecutor = await manager.GetExecutorAsync(cancellationToken: cts.Token);

monitor.Set(ISchemaDefinition.DefaultName, new RequestExecutorSetup
{
SchemaBuilder = SchemaBuilder.New()
.AddQueryType(d => d.Field("field2").Resolve("")),
Pipeline = { new RequestMiddlewareConfiguration((_, _) => _ => ValueTask.CompletedTask) }
});

executorEvictedResetEvent.Wait(cts.Token);

var executorAfterEviction = await manager.GetExecutorAsync(cancellationToken: cts.Token);

Assert.NotSame(initialExecutor, executorAfterEviction);

executorAfterEviction.Schema.MatchInlineSnapshot(
"""
schema {
query: ObjectType
}
type ObjectType {
field2: String
}
""");

cts.Dispose();
}

private sealed class TestOptionsMonitor : IRequestExecutorOptionsMonitor
{
private readonly Dictionary<string, RequestExecutorSetup> _setups = new();
private readonly List<Action<string>> _listeners = new();

public void Set(string schemaName, RequestExecutorSetup setup)
{
_setups[schemaName] = setup;

foreach (var listener in _listeners)
{
listener(schemaName);
}
}

public RequestExecutorSetup Get(string schemaName)
{
return _setups[schemaName];
}

public IDisposable OnChange(Action<string> listener)
{
_listeners.Add(listener);
return new Unsubscriber(this, listener);
}

private void Unsubscribe(Action<string> listener)
{
_listeners.Remove(listener);
}

private sealed class Unsubscriber(
TestOptionsMonitor monitor,
Action<string> listener)
: IDisposable
{
public void Dispose() => monitor.Unsubscribe(listener);
}
}
}
Loading