Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
81 changes: 53 additions & 28 deletions src/HotChocolate/Core/src/Types/Execution/RequestExecutorManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -160,45 +160,70 @@ private async Task<RegisteredExecutor> CreateRequestExecutorAsync(
_applicationServices);

var typeModuleChangeMonitor = new TypeModuleChangeMonitor(this, context.SchemaName);
ServiceProvider? schemaServices = null;

// If there are any type modules, we will register them with the
// type module change monitor.
// The module will track if type modules signal changes to the schema and
// start a schema eviction.
foreach (var typeModule in setup.TypeModules)
try
{
typeModuleChangeMonitor.Register(typeModule);
}
// If there are any type modules, we will register them with the
// type module change monitor.
// The module will track if type modules signal changes to the schema and
// start a schema eviction.
foreach (var typeModule in setup.TypeModules)
{
typeModuleChangeMonitor.Register(typeModule);
}

var schemaServices =
await CreateSchemaServicesAsync(context, setup, typeModuleChangeMonitor, cancellationToken)
.ConfigureAwait(false);
schemaServices =
await CreateSchemaServicesAsync(context, setup, typeModuleChangeMonitor, cancellationToken)
.ConfigureAwait(false);

var registeredExecutor = new RegisteredExecutor(
schemaServices.GetRequiredService<IRequestExecutor>(),
schemaServices,
schemaServices.GetRequiredService<IExecutionDiagnosticEvents>(),
setup,
typeModuleChangeMonitor,
setup.EvictionTimeout);
var registeredExecutor = new RegisteredExecutor(
schemaServices.GetRequiredService<IRequestExecutor>(),
schemaServices,
schemaServices.GetRequiredService<IExecutionDiagnosticEvents>(),
setup,
typeModuleChangeMonitor,
setup.EvictionTimeout);

var executor = registeredExecutor.Executor;
var executor = registeredExecutor.Executor;

await OnRequestExecutorCreatedAsync(context, executor, setup, cancellationToken)
.ConfigureAwait(false);
await OnRequestExecutorCreatedAsync(context, executor, setup, cancellationToken)
.ConfigureAwait(false);

await WarmupExecutorAsync(executor, isInitialCreation, cancellationToken).ConfigureAwait(false);
await WarmupExecutorAsync(executor, isInitialCreation, cancellationToken).ConfigureAwait(false);

_executors[schemaName] = registeredExecutor;
_executors[schemaName] = registeredExecutor;

registeredExecutor.DiagnosticEvents.ExecutorCreated(
schemaName,
registeredExecutor.Executor);
registeredExecutor.DiagnosticEvents.ExecutorCreated(
schemaName,
registeredExecutor.Executor);

_events.RaiseEvent(
RequestExecutorEvent.Created(registeredExecutor.Executor));
_events.RaiseEvent(
RequestExecutorEvent.Created(registeredExecutor.Executor));

return registeredExecutor;
return registeredExecutor;
Comment thread
tobias-tengler marked this conversation as resolved.
}
catch
{
// If the executor creation fails, we have to dispose of the change monitor so that
// its event subscriptions do not leak. A leaked subscription would cause subsequent
// type changes to trigger additional (also failing) rebuild attempts.
typeModuleChangeMonitor.Dispose();

if (schemaServices is not null)
{
try
{
await schemaServices.DisposeAsync().ConfigureAwait(false);
}
catch
{
// A dispose failure must not mask the original executor creation error.
}
Comment thread
tobias-tengler marked this conversation as resolved.
Dismissed
}

throw;
}
}

private async Task UpdateRequestExecutorAsync(string schemaName, RegisteredExecutor previousExecutor)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,76 @@ public async Task EvictExecutor_With_Custom_TypeInspector_Should_Rebuild_Without
Assert.NotSame(initialExecutor, rebuiltExecutor);
}

[Fact]
public async Task OnTypesChanged_Should_Not_Grow_CreateTypes_Calls_Exponentially_When_Type_Instance_Registered()
{
// arrange
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
var typeModule = new CountingTypeModule();

var manager = new ServiceCollection()
.AddGraphQL()
.AddTypeModule(_ => typeModule)
.AddType(new ObjectType<FooType>())
.AddQueryType(d => d.Field("foo").Resolve(""))
.Services
.BuildServiceProvider()
.GetRequiredService<RequestExecutorManager>();

await manager.GetExecutorAsync(cancellationToken: cts.Token);
var createCallsAfterInitial = typeModule.CreateTypesCallCount;

// act
for (var i = 1; i <= 3; i++)
{
typeModule.TriggerChange();

// The rebuild throws (the type instance is already initialized), so no Evicted
// event is raised. The only observable signal of the rebuild attempt is the
// CreateTypesAsync counter, so wait until this trigger's attempt has happened.
var expectedCalls = createCallsAfterInitial + i;
await SpinUntilAsync(() => typeModule.CreateTypesCallCount >= expectedCalls, cts.Token);
}

// give leaked subscriptions time to surface additional rebuild attempts
await Task.Delay(200, cts.Token);

// assert
var createCallsFromTriggers = typeModule.CreateTypesCallCount - createCallsAfterInitial;
Assert.Equal(3, createCallsFromTriggers);
}

private static async Task SpinUntilAsync(Func<bool> condition, CancellationToken cancellationToken)
{
while (!condition())
{
cancellationToken.ThrowIfCancellationRequested();
await Task.Delay(10, cancellationToken);
}
}

private sealed class CountingTypeModule : TypeModule
{
private int _createTypesCallCount;

public int CreateTypesCallCount => _createTypesCallCount;

public void TriggerChange() => OnTypesChanged();

public override ValueTask<IReadOnlyCollection<ITypeSystemMember>> CreateTypesAsync(
IDescriptorContext context,
CancellationToken cancellationToken)
{
Interlocked.Increment(ref _createTypesCallCount);
return base.CreateTypesAsync(context, cancellationToken);
}
}

private sealed class FooType
{
public string Bar() => "baz";
}

#pragma warning disable CS9113 // Parameter is unread.
private sealed class CustomWarmupTask(IDocumentCache documentCache, SomeService service) : IRequestExecutorWarmupTask
#pragma warning restore CS9113 // Parameter is unread.
Expand Down
Loading