diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/ExecutorSession.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/ExecutorSession.cs index 82ac4b037df..b7bc715bc95 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/ExecutorSession.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/ExecutorSession.cs @@ -91,6 +91,7 @@ public async Task ExecuteSingleAsync( var requestBuilder = OperationRequestBuilder.From(request); requestBuilder.SetFlags(flags); + requestBuilder.SetServices(context.RequestServices); await _requestInterceptor.OnCreateAsync(context, _executor, requestBuilder, context.RequestAborted); @@ -116,6 +117,7 @@ public async Task ExecuteOperationBatchAsync( var requestBuilder = OperationRequestBuilder.From(request); requestBuilder.SetOperationName(operationNames[i]); requestBuilder.SetFlags(flags); + requestBuilder.SetServices(context.RequestServices); await _requestInterceptor.OnCreateAsync(context, _executor, requestBuilder, context.RequestAborted); @@ -144,6 +146,7 @@ public async Task ExecuteBatchAsync( { var requestBuilder = OperationRequestBuilder.From(requests[i]); requestBuilder.SetFlags(flags); + requestBuilder.SetServices(context.RequestServices); await _requestInterceptor.OnCreateAsync(context, _executor, requestBuilder, context.RequestAborted); diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HotChocolate.AspNetCore.Pipeline.csproj b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HotChocolate.AspNetCore.Pipeline.csproj index fcd1760b60f..e55fcf87238 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HotChocolate.AspNetCore.Pipeline.csproj +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HotChocolate.AspNetCore.Pipeline.csproj @@ -14,6 +14,7 @@ + diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLServerOptions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLServerOptions.cs index c603a0f87eb..dbaa874ca4e 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLServerOptions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Options/GraphQLServerOptions.cs @@ -54,5 +54,5 @@ public sealed class GraphQLServerOptions /// /// Defines if request batching is enabled. /// - public bool EnableBatching { get; set; } + public bool EnableBatching { get; set; } = true; } diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/OperationSession.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/OperationSession.cs index d55e4655d6d..a2e8db84408 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/OperationSession.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Subscriptions/OperationSession.cs @@ -38,7 +38,7 @@ public OperationSession( public bool IsCompleted { get; private set; } public void BeginExecute(GraphQLRequest request, CancellationToken cancellationToken) - => SendResultsAsync(request, cancellationToken).FireAndForget(); + => _ = SendResultsAsync(request, cancellationToken); private async Task SendResultsAsync(GraphQLRequest request, CancellationToken cancellationToken) { diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/HeaderUtilities.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/HeaderUtilities.cs index 9725273c505..e9d1ebfcb09 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/HeaderUtilities.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Utilities/HeaderUtilities.cs @@ -1,7 +1,7 @@ -using System.Collections.Concurrent; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using HotChocolate.Caching.Memory; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -13,8 +13,8 @@ namespace HotChocolate.AspNetCore.Utilities; /// internal static class HeaderUtilities { - private static readonly ConcurrentDictionary s_cache = - new(StringComparer.Ordinal); + private static readonly Cache s_headerCache = new(128); + private static readonly Cache s_mediaTypeCache = new(128); public static readonly AcceptMediaType[] GraphQLResponseContentTypes = [ @@ -42,109 +42,95 @@ public static AcceptHeaderResult GetAcceptHeader(HttpRequest request) return new AcceptHeaderResult([]); } - string[] innerArray; - if (count == 1) { var headerValue = value[0]!; - if (TryParseMediaType(headerValue, out var parsedValue)) + if (s_headerCache.TryGet(headerValue, out var cached)) { - return new AcceptHeaderResult([parsedValue]); + return cached; } - // note: this is a workaround for now. we need to parse this properly. - if (headerValue.IndexOf(',', StringComparison.Ordinal) != -1) - { - innerArray = headerValue.Split(','); - goto MULTI_VALUES; - } - - return new AcceptHeaderResult(headerValue); - } + var result = ParseHeaderValue(headerValue); - innerArray = value!; - -MULTI_VALUES: - ref var searchSpace = ref MemoryMarshal.GetReference(innerArray.AsSpan()); - var parsedValues = new AcceptMediaType[innerArray.Length]; - var p = 0; - - for (var i = 0; i < innerArray.Length; i++) - { - var mediaType = Unsafe.Add(ref searchSpace, i); - if (TryParseMediaType(mediaType, out var parsedValue)) - { - parsedValues[p++] = parsedValue; - } - else + if (!result.HasError) { - return new AcceptHeaderResult(mediaType); + s_headerCache.TryAdd(headerValue, result); } - } - if (parsedValues.Length > p) - { - Array.Resize(ref parsedValues, p); + return result; } - return new AcceptHeaderResult(parsedValues); + return ParseMultipleHeaderValues(value!); } return new AcceptHeaderResult([]); } - private static bool TryParseMediaType(string s, out AcceptMediaType value) + private static AcceptHeaderResult ParseHeaderValue(string headerValue) { - MakeSpace(); - - // first we try to look up the parsed header in the cache. - // if we find it the string was a valid header value and - // we return it. - if (s_cache.TryGetValue(s, out var entry)) + if (TryParseMediaType(headerValue, out var parsedValue)) { - value = entry.Value; - return true; + return new AcceptHeaderResult([parsedValue]); } - // if not we will try to parse it. - if (MediaTypeHeaderValue.TryParse(s, out var parsedValue)) + // note: this is a workaround for now. we need to parse this properly. + if (headerValue.IndexOf(',', StringComparison.Ordinal) != -1) { - entry = s_cache.GetOrAdd(s, k => new CacheEntry(k, parsedValue)); - value = entry.Value; - return true; + return ParseMultipleHeaderValues(headerValue.Split(',')); } - value = default; - return false; + return new AcceptHeaderResult(headerValue); } - private static void MakeSpace() + private static AcceptHeaderResult ParseMultipleHeaderValues(string[] innerArray) { - // if we reach the maximum available space we will remove around 20% of the cached items. - if (s_cache.Count > 100) + ref var searchSpace = ref MemoryMarshal.GetReference(innerArray.AsSpan()); + var parsedValues = new AcceptMediaType[innerArray.Length]; + var p = 0; + + for (var i = 0; i < innerArray.Length; i++) { - foreach (var entry in s_cache.Values.OrderBy(t => t.CreatedAt).Take(20)) + var mediaType = Unsafe.Add(ref searchSpace, i); + if (TryParseMediaType(mediaType, out var parsedValue)) { - s_cache.TryRemove(entry.Key, out _); + parsedValues[p++] = parsedValue; + } + else + { + return new AcceptHeaderResult(mediaType); } } + + if (parsedValues.Length > p) + { + Array.Resize(ref parsedValues, p); + } + + return new AcceptHeaderResult(parsedValues); } - private readonly struct CacheEntry + private static bool TryParseMediaType(string s, out AcceptMediaType value) { - public CacheEntry(string key, MediaTypeHeaderValue value) + if (s_mediaTypeCache.TryGet(s, out var cached)) { - Key = key; - Value = new AcceptMediaType(value.Type, value.SubType, value.Quality, value.Charset); - CreatedAt = DateTime.UtcNow; + value = cached; + return true; } - public string Key { get; } - - public AcceptMediaType Value { get; } + if (MediaTypeHeaderValue.TryParse(s, out var parsedValue)) + { + value = new AcceptMediaType( + parsedValue.Type, + parsedValue.SubType, + parsedValue.Quality, + parsedValue.Charset); + s_mediaTypeCache.TryAdd(s, value); + return true; + } - public DateTime CreatedAt { get; } + value = default; + return false; } internal readonly struct AcceptHeaderResult diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/EventStreamResultFormatter.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/EventStreamResultFormatter.cs index e5905bd7649..10453a2c904 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/EventStreamResultFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/EventStreamResultFormatter.cs @@ -318,7 +318,7 @@ private void EnsureKeepAlive() if (DateTime.UtcNow - _lastWriteTime >= s_keepAlivePeriod) { - WriteKeepAliveAsync().FireAndForget(); + _ = WriteKeepAliveAsync(); } async Task WriteKeepAliveAsync() diff --git a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonLinesResultFormatter.cs b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonLinesResultFormatter.cs index 382ffaf74db..1ae4b7760a3 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonLinesResultFormatter.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Formatters/JsonLinesResultFormatter.cs @@ -272,7 +272,7 @@ private void EnsureKeepAlive() if (DateTime.UtcNow - _lastWriteTime >= s_keepAlivePeriod) { - WriteKeepAliveAsync().FireAndForget(); + _ = WriteKeepAliveAsync(); } async Task WriteKeepAliveAsync() diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs index ee2c2cf83a6..45ae3f879e3 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs @@ -1,5 +1,6 @@ // ReSharper disable IntroduceOptionalParameters.Global +using System.Diagnostics; using System.Net.Http.Headers; using System.Text; using System.Text.Json; @@ -187,6 +188,8 @@ private static ByteArrayContent CreatePostContent( request.Body.WriteTo(jsonWriter); jsonWriter.Flush(); + Debug.WriteLine(Encoding.UTF8.GetString(arrayWriter.WrittenSpan)); + var internalBuffer = PooledArrayWriterMarshal.GetUnderlyingBuffer(arrayWriter); var content = new ByteArrayContent(internalBuffer, 0, arrayWriter.Length); content.Headers.ContentType = new MediaTypeHeaderValue(ContentType.Json, "utf-8"); diff --git a/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs b/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs index ba481f5bdfc..0b3127da5c9 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Sockets.Client/Protocols/GraphQLOverWebSocket/GraphQLOverWebSocketProtocolHandler.cs @@ -161,7 +161,7 @@ public void TrySendCompleteMessage() { if (!_completed) { - TrySendCompleteMessageInternalAsync(socket, id).FireAndForget(); + _ = TrySendCompleteMessageInternalAsync(socket, id); _completed = true; } } diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/PooledRequestContext.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/PooledRequestContext.cs index 46f24e77b9c..b7a56da2589 100644 --- a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/PooledRequestContext.cs +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/PooledRequestContext.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using HotChocolate.Features; using HotChocolate.Language; @@ -47,7 +46,7 @@ public PooledRequestContext() public override IFeatureCollection Features => _features; /// - public override IDictionary ContextData { get; } = new ConcurrentDictionary(); + public override IDictionary ContextData { get; } = new RequestContextData(); /// /// Initializes the request context after renting it from the pool. diff --git a/src/HotChocolate/Core/src/Execution.Abstractions/Execution/RequestContextData.cs b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/RequestContextData.cs new file mode 100644 index 00000000000..21c36813cd5 --- /dev/null +++ b/src/HotChocolate/Core/src/Execution.Abstractions/Execution/RequestContextData.cs @@ -0,0 +1,198 @@ +using System.Collections; + +namespace HotChocolate.Execution; + +/// +/// A thread-safe dictionary for request context data, optimized for pooling. +/// Uses a simple lock over rather than +/// +/// to avoid allocation on . +/// +public sealed class RequestContextData : IDictionary, IReadOnlyDictionary +{ +#if NET9_0_OR_GREATER + private readonly Lock _lock = new(); +#else + private readonly object _lock = new(); +#endif + private readonly Dictionary _dictionary = []; + + /// + public object? this[string key] + { + get + { + lock (_lock) + { + return _dictionary[key]; + } + } + set + { + lock (_lock) + { + _dictionary[key] = value; + } + } + } + + /// + public int Count + { + get + { + lock (_lock) + { + return _dictionary.Count; + } + } + } + + /// + public bool IsReadOnly => false; + + /// + public ICollection Keys + { + get + { + lock (_lock) + { + return [.. _dictionary.Keys]; + } + } + } + + /// + public ICollection Values + { + get + { + lock (_lock) + { + return [.. _dictionary.Values]; + } + } + } + + /// + IEnumerable IReadOnlyDictionary.Keys + { + get + { + lock (_lock) + { + return [.. _dictionary.Keys]; + } + } + } + + /// + IEnumerable IReadOnlyDictionary.Values + { + get + { + lock (_lock) + { + return [.. _dictionary.Values]; + } + } + } + + /// + public void Add(string key, object? value) + { + lock (_lock) + { + _dictionary.Add(key, value); + } + } + + /// + public void Add(KeyValuePair item) + { + lock (_lock) + { + _dictionary.Add(item.Key, item.Value); + } + } + + /// + public void Clear() + { + lock (_lock) + { + _dictionary.Clear(); + } + } + + /// + public bool Contains(KeyValuePair item) + { + lock (_lock) + { + return ((ICollection>)_dictionary).Contains(item); + } + } + + /// + public bool ContainsKey(string key) + { + lock (_lock) + { + return _dictionary.ContainsKey(key); + } + } + + /// + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + lock (_lock) + { + ((ICollection>)_dictionary).CopyTo(array, arrayIndex); + } + } + + /// + public bool Remove(string key) + { + lock (_lock) + { + return _dictionary.Remove(key); + } + } + + /// + public bool Remove(KeyValuePair item) + { + lock (_lock) + { + return ((ICollection>)_dictionary).Remove(item); + } + } + + /// + public bool TryGetValue(string key, out object? value) + { + lock (_lock) + { + return _dictionary.TryGetValue(key, out value); + } + } + + /// + public IEnumerator> GetEnumerator() + { + KeyValuePair[] snapshot; + + lock (_lock) + { + snapshot = [.. _dictionary]; + } + + return ((IEnumerable>)snapshot).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); +} diff --git a/src/HotChocolate/Core/src/Subscriptions.Postgres/ContinuousTask.cs b/src/HotChocolate/Core/src/Subscriptions.Postgres/ContinuousTask.cs index a63524ad4ed..84f765dcaac 100644 --- a/src/HotChocolate/Core/src/Subscriptions.Postgres/ContinuousTask.cs +++ b/src/HotChocolate/Core/src/Subscriptions.Postgres/ContinuousTask.cs @@ -18,7 +18,7 @@ public ContinuousTask(Func handler, TimeProvider timePr // We do not use Task.Factory.StartNew here because RunContinuously is an async method and // the LongRunning flag only works until the first await. - RunContinuously().FireAndForget(); + _ = RunContinuously(); } public CancellationToken Completion => _completion.Token; diff --git a/src/HotChocolate/Core/src/Subscriptions/DefaultTopic.cs b/src/HotChocolate/Core/src/Subscriptions/DefaultTopic.cs index 9db210f4f4b..77b1b8fff08 100644 --- a/src/HotChocolate/Core/src/Subscriptions/DefaultTopic.cs +++ b/src/HotChocolate/Core/src/Subscriptions/DefaultTopic.cs @@ -195,7 +195,7 @@ internal async ValueTask ConnectAsync(CancellationToken ct = default) } private void BeginProcessing(IAsyncDisposable session) - => ProcessMessagesSessionAsync(session).FireAndForget(); + => _ = ProcessMessagesSessionAsync(session); private async Task ProcessMessagesSessionAsync(IAsyncDisposable session) { diff --git a/src/HotChocolate/Core/src/Types/Execution/DefaultRequestExecutor.cs b/src/HotChocolate/Core/src/Types/Execution/DefaultRequestExecutor.cs index 25757de7d35..1a9a959e6b5 100644 --- a/src/HotChocolate/Core/src/Types/Execution/DefaultRequestExecutor.cs +++ b/src/HotChocolate/Core/src/Types/Execution/DefaultRequestExecutor.cs @@ -302,13 +302,28 @@ private async IAsyncEnumerable ExecuteBatchStream( private static IOperationRequest WithServices( IOperationRequest request, - IServiceProvider services) => - request switch + IServiceProvider services) + { + switch (request) { - OperationRequest op => op.WithServices(services), - VariableBatchRequest vb => vb.WithServices(services), - _ => throw new InvalidOperationException("Unexpected request type.") - }; + case OperationRequest op: + if (ReferenceEquals(op.Services, services)) + { + return op; + } + return op.WithServices(services); + + case VariableBatchRequest vb: + if (ReferenceEquals(vb.Services, services)) + { + return vb; + } + return vb.WithServices(services); + + default: + throw new InvalidOperationException("Unexpected request type."); + } + } private async Task ExecuteBatchItemAsync( IOperationRequest request, diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs index d95750915f4..39e66c2b45c 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/Tasks/ResolverTask.cs @@ -84,6 +84,6 @@ public ExecutionTaskKind Kind public void BeginExecute(CancellationToken cancellationToken) { Status = ExecutionTaskStatus.Running; - ExecuteAsync(cancellationToken).FireAndForget(); + _ = ExecuteAsync(cancellationToken); } } diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs index 411a7180b8b..53ae2efe59d 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Execute.cs @@ -104,8 +104,16 @@ private async Task ExecuteInternalAsync(IExecutionTask?[] buffer) private async Task WaitForTask(uint taskId) { - while (!_completed.ContainsKey(taskId)) + while (true) { + lock (_sync) + { + if (_completed.Contains(taskId)) + { + return; + } + } + // we are waiting for completion of the current task // so we force the `TryDispatchOrComplete` to seek completion // even though the _work backlog might still have unprocessed diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs index df826dc5f93..0768be46150 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.Pooling.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using System.Runtime.CompilerServices; using HotChocolate.Execution.Instrumentation; using HotChocolate.Fetching; @@ -19,7 +18,7 @@ internal sealed partial class WorkScheduler(OperationContext operationContext) private OperationResultBuilder _result = null!; private IErrorHandler _errorHandler = null!; private IExecutionDiagnosticEvents _diagnosticEvents = null!; - private readonly ConcurrentDictionary _completed = new(); + private readonly HashSet _completed = []; private uint _nextId = 1; private CancellationToken _ct; diff --git a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs index 574e44a2046..d1b4508adf9 100644 --- a/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs +++ b/src/HotChocolate/Core/src/Types/Execution/Processing/WorkScheduler.cs @@ -91,7 +91,11 @@ public void Complete(IExecutionTask task) // complete is thread-safe if (work.Complete()) { - _completed.TryAdd(task.Id, true); + lock (_sync) + { + _completed.Add(task.Id); + } + _signal.Set(); } } diff --git a/src/HotChocolate/Core/src/Types/Fetching/BatchDispatcher.cs b/src/HotChocolate/Core/src/Types/Fetching/BatchDispatcher.cs index 4855faef755..961ba842fc8 100644 --- a/src/HotChocolate/Core/src/Types/Fetching/BatchDispatcher.cs +++ b/src/HotChocolate/Core/src/Types/Fetching/BatchDispatcher.cs @@ -92,7 +92,7 @@ private void EnsureCoordinatorIsRunning() if (Interlocked.CompareExchange(ref _isCoordinatorRunning, 1, 0) == 0) { - CoordinatorAsync(_coordinatorCts.Token).FireAndForget(); + _ = CoordinatorAsync(_coordinatorCts.Token); } } diff --git a/src/HotChocolate/Core/src/Types/Fetching/ExecutionDataLoaderScope.cs b/src/HotChocolate/Core/src/Types/Fetching/ExecutionDataLoaderScope.cs index 737e035edfc..5fac2f552ee 100644 --- a/src/HotChocolate/Core/src/Types/Fetching/ExecutionDataLoaderScope.cs +++ b/src/HotChocolate/Core/src/Types/Fetching/ExecutionDataLoaderScope.cs @@ -1,4 +1,3 @@ -using System.Collections.Concurrent; using GreenDonut; using GreenDonut.DependencyInjection; using HotChocolate.Properties; @@ -11,7 +10,8 @@ internal sealed class ExecutionDataLoaderScope( IReadOnlyDictionary registrations) : IDataLoaderScope { - private readonly ConcurrentDictionary _dataLoaders = new(); + private readonly Lock _sync = new(); + private Dictionary? _dataLoaders; private readonly IServiceProvider _serviceProvider = new DataLoaderServiceProvider(serviceProvider, batchScheduler); @@ -19,9 +19,22 @@ public T GetDataLoader(DataLoaderFactory createDataLoader, string? name = { name ??= CreateKey(); - if (_dataLoaders.GetOrAdd(name, _ => createDataLoader(_serviceProvider)) is T dataLoader) + IDataLoader dataLoader; + + lock (_sync) + { + _dataLoaders ??= []; + + if (!_dataLoaders.TryGetValue(name, out dataLoader!)) + { + dataLoader = createDataLoader(_serviceProvider); + _dataLoaders.Add(name, dataLoader); + } + } + + if (dataLoader is T typed) { - return dataLoader; + return typed; } throw new RegisterDataLoaderException( @@ -32,7 +45,23 @@ public T GetDataLoader(DataLoaderFactory createDataLoader, string? name = } public T GetDataLoader() where T : IDataLoader - => (T)_dataLoaders.GetOrAdd(CreateKey(), _ => CreateDataLoader()); + { + var key = CreateKey(); + + lock (_sync) + { + _dataLoaders ??= []; + + if (_dataLoaders.TryGetValue(key, out var existing)) + { + return (T)existing; + } + + var created = CreateDataLoader(); + _dataLoaders.Add(key, created); + return created; + } + } private T CreateDataLoader() where T : IDataLoader { diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/LockStoreBenchmark.cs b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/LockStoreBenchmark.cs new file mode 100644 index 00000000000..c26f36c9336 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/LockStoreBenchmark.cs @@ -0,0 +1,238 @@ +using System; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; + +namespace Fusion.Execution.Benchmarks; + +[ThreadingDiagnoser] +[MemoryDiagnoser] +[InProcess] +[WarmupCount(3)] +[IterationCount(8)] +[InvocationCount(1)] +public class LockStoreBenchmark +{ + private const int DataLength = 256; + + private readonly object _monitorGate = new(); + private readonly Lock _runtimeLock = new(); + private readonly ReaderWriterLockSlim _readerWriterLock = new(LockRecursionPolicy.NoRecursion); + private SpinLock _spinLock = new(enableThreadOwnerTracking: false); + + private int[] _operationKinds = null!; + private int[] _operationIndexes = null!; + private int[] _data = null!; + private ParallelOptions _parallelOptions = null!; + private int _sink; + + [Params(8, 32)] + public int Threads { get; set; } + + [Params(10, 50)] + public int WritePercent { get; set; } + + [Params(0, 64)] + public int CriticalSectionWork { get; set; } + + [Params(200_000)] + public int Operations { get; set; } + + [GlobalSetup] + public void Setup() + { + _parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Threads }; + _operationKinds = new int[Operations]; + _operationIndexes = new int[Operations]; + _data = new int[DataLength]; + + var seed = unchecked((uint)(17 + Threads * 397 + WritePercent * 17 + CriticalSectionWork)); + + for (var i = 0; i < Operations; i++) + { + seed = Next(seed); + _operationKinds[i] = (int)(seed % 100) < WritePercent ? 1 : 0; + + seed = Next(seed); + _operationIndexes[i] = (int)(seed % DataLength); + } + } + + [IterationSetup] + public void IterationSetup() + { + Array.Clear(_data); + _sink = 0; + } + + [GlobalCleanup] + public void Cleanup() => _readerWriterLock.Dispose(); + + [Benchmark(Baseline = true)] + public int MonitorLock() => Run( + read: i => + { + lock (_monitorGate) + { + return ReadCore(i); + } + }, + write: i => + { + lock (_monitorGate) + { + return WriteCore(i); + } + }); + + [Benchmark] + public int RuntimeLock() => Run( + read: i => + { + lock (_runtimeLock) + { + return ReadCore(i); + } + }, + write: i => + { + lock (_runtimeLock) + { + return WriteCore(i); + } + }); + + [Benchmark] + public int ReaderWriterLockSlim() => Run( + read: i => + { + _readerWriterLock.EnterReadLock(); + + try + { + return ReadCore(i); + } + finally + { + _readerWriterLock.ExitReadLock(); + } + }, + write: i => + { + _readerWriterLock.EnterWriteLock(); + + try + { + return WriteCore(i); + } + finally + { + _readerWriterLock.ExitWriteLock(); + } + }); + + [Benchmark] + public int SpinLock() => Run( + read: i => + { + var lockTaken = false; + + try + { + _spinLock.Enter(ref lockTaken); + return ReadCore(i); + } + finally + { + if (lockTaken) + { + _spinLock.Exit(useMemoryBarrier: true); + } + } + }, + write: i => + { + var lockTaken = false; + + try + { + _spinLock.Enter(ref lockTaken); + return WriteCore(i); + } + finally + { + if (lockTaken) + { + _spinLock.Exit(useMemoryBarrier: true); + } + } + }); + + private int Run(Func read, Func write) + { + var total = 0; + + Parallel.For( + 0, + Operations, + _parallelOptions, + static () => 0, + (i, _, local) => + { + if (_operationKinds[i] == 1) + { + local = unchecked(local + write(i)); + } + else + { + local = unchecked(local + read(i)); + } + + return local; + }, + local => Interlocked.Add(ref total, local)); + + _sink = total; + return _sink; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int ReadCore(int operationIndex) + { + var dataIndex = _operationIndexes[operationIndex]; + var value = _data[dataIndex]; + return BusyWork(unchecked(value + dataIndex)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int WriteCore(int operationIndex) + { + var dataIndex = _operationIndexes[operationIndex]; + var value = BusyWork(unchecked(_data[dataIndex] + 1 + dataIndex)); + _data[dataIndex] = value; + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private int BusyWork(int value) + { + var work = CriticalSectionWork; + + for (var i = 0; i < work; i++) + { + value = unchecked((value * 1664525) + 1013904223); + } + + return value; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static uint Next(uint x) + { + x ^= x << 13; + x ^= x >> 17; + x ^= x << 5; + return x; + } +} diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/Program.cs b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/Program.cs index 01aeb56b5e3..8f8a9dd821b 100644 --- a/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/Program.cs +++ b/src/HotChocolate/Fusion-vnext/benchmarks/Fusion.Execution.Benchmarks/Program.cs @@ -5,4 +5,13 @@ var config = DefaultConfig.Instance .WithOption(ConfigOptions.DisableOptimizationsValidator, true); -BenchmarkRunner.Run(config); +if (args.Length == 0) +{ + BenchmarkRunner.Run(config); +} +else +{ + BenchmarkSwitcher + .FromAssembly(typeof(GraphQLQueryBenchmark).Assembly) + .Run(args, config); +} diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/k6/Directory.Build.props b/src/HotChocolate/Fusion-vnext/benchmarks/k6/Directory.Build.props index 8f615be8f19..404a1ca9f4d 100644 --- a/src/HotChocolate/Fusion-vnext/benchmarks/k6/Directory.Build.props +++ b/src/HotChocolate/Fusion-vnext/benchmarks/k6/Directory.Build.props @@ -2,8 +2,8 @@ - net9.0 - net9.0 + net10.0 + net10.0 enable enable diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Accounts/Program.cs b/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Accounts/Program.cs index 089b74608bf..953c655a766 100644 --- a/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Accounts/Program.cs +++ b/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Accounts/Program.cs @@ -5,11 +5,11 @@ var builder = WebApplication.CreateBuilder(args); builder - .AddGraphQL("accounts-api") + .AddGraphQL("accounts-api", disableDefaultSecurity: true) .AddAccountTypes(); var app = builder.Build(); -app.MapGraphQL(); +app.MapGraphQLHttp(); await app.RunWithGraphQLCommandsAsync(args); diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Gateway/BufferPoolDiagnostics.cs b/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Gateway/BufferPoolDiagnostics.cs new file mode 100644 index 00000000000..e66527299c9 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Gateway/BufferPoolDiagnostics.cs @@ -0,0 +1,151 @@ +using System.Diagnostics.Tracing; + +namespace eShop.Gateway; + +/// +/// Listens to FixedSizeArrayPool ETW events and tracks rent/return balance +/// to detect leaks or pool exhaustion. +/// +internal sealed class BufferPoolDiagnostics : EventListener, IHostedService +{ + private readonly ILogger _logger; + private readonly Timer _timer; + + private long _rented; + private long _returned; + private long _poolExhausted; + private long _bufferDropped; + private long _bufferAllocated; + private long _poolTrimmed; + private int _lastTrimRemaining; + private int _lastTrimInUse; + private int _peakInUse; + + public BufferPoolDiagnostics(ILogger logger) + { + _logger = logger; + _timer = new Timer(LogSnapshot, null, Timeout.Infinite, Timeout.Infinite); + } + + protected override void OnEventSourceCreated(EventSource eventSource) + { + if (eventSource.Name == "HotChocolate-Buffers-FixedSizeArrayPool") + { + EnableEvents(eventSource, EventLevel.Verbose); + } + } + + protected override void OnEventWritten(EventWrittenEventArgs e) + { + // Event IDs from FixedSizeArrayPoolEventSource: + // 1 = PoolCreated (PoolId, Chunks, TotalBytes) + // 2 = BufferRented (BufferId, Size, PoolId, InUse) + // 3 = BufferReturned (BufferId, Size, PoolId, InUse) + // 4 = PoolExhausted (PoolId, MaxChunks) + // 5 = BufferDropped (BufferId, Size, PoolId) + // 6 = BufferAllocated (BufferId, Size, PoolId) + // 7 = PoolTrimmed (PoolId, Trimmed, Remaining, InUse) + + switch (e.EventId) + { + case 2: // BufferRented + Interlocked.Increment(ref _rented); + if (e.Payload is { Count: >= 4 } && e.Payload[3] is int inUseRent) + { + UpdatePeakInUse(inUseRent); + } + break; + + case 3: // BufferReturned + Interlocked.Increment(ref _returned); + break; + + case 4: // PoolExhausted + Interlocked.Increment(ref _poolExhausted); + break; + + case 5: // BufferDropped + Interlocked.Increment(ref _bufferDropped); + break; + + case 6: // BufferAllocated + Interlocked.Increment(ref _bufferAllocated); + break; + + case 7: // PoolTrimmed + Interlocked.Increment(ref _poolTrimmed); + if (e.Payload is { Count: >= 4 }) + { + if (e.Payload[2] is int remaining) + { + _lastTrimRemaining = remaining; + } + if (e.Payload[3] is int inUseTrim) + { + _lastTrimInUse = inUseTrim; + } + } + break; + } + } + + private void UpdatePeakInUse(int inUse) + { + int current; + do + { + current = _peakInUse; + if (inUse <= current) + { + return; + } + } + while (Interlocked.CompareExchange(ref _peakInUse, inUse, current) != current); + } + + private void LogSnapshot(object? state) + { + var rented = Interlocked.Read(ref _rented); + var returned = Interlocked.Read(ref _returned); + var exhausted = Interlocked.Read(ref _poolExhausted); + var dropped = Interlocked.Read(ref _bufferDropped); + var allocated = Interlocked.Read(ref _bufferAllocated); + var trimmed = Interlocked.Read(ref _poolTrimmed); + var trimRemaining = _lastTrimRemaining; + var trimInUse = _lastTrimInUse; + var peak = _peakInUse; + var outstanding = rented - returned; + + _logger.LogInformation( + "[BufferPool] Rented={Rented}, Returned={Returned}, Outstanding={Outstanding}, " + + "PeakInUse={PeakInUse}, PoolExhausted={PoolExhausted}, " + + "Dropped={Dropped}, Allocated={Allocated}, " + + "Trimmed={Trimmed}, TrimRemaining={TrimRemaining}, TrimInUse={TrimInUse}", + rented, returned, outstanding, peak, exhausted, dropped, allocated, + trimmed, trimRemaining, trimInUse); + } + + public Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation( + "[BufferPool] Diagnostics started — pool capacity=128, chunk size=128KB"); + + // Log every 5 seconds during the benchmark run. + _timer.Change(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5)); + return Task.CompletedTask; + } + + public Task StopAsync(CancellationToken cancellationToken) + { + _timer.Change(Timeout.Infinite, Timeout.Infinite); + LogSnapshot(null); + _logger.LogInformation("[BufferPool] Diagnostics stopped — final snapshot logged above"); + return Task.CompletedTask; + } + + public override void Dispose() + { + _timer.Dispose(); + base.Dispose(); + } +} diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Gateway/Program.cs b/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Gateway/Program.cs index f99a1abb744..e14429a81db 100644 --- a/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Gateway/Program.cs +++ b/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Gateway/Program.cs @@ -1,428 +1,21 @@ -using System.Diagnostics.Tracing; -using System.Threading.Channels; -using HotChocolate; -using HotChocolate.AspNetCore; -using HotChocolate.Execution; -using HotChocolate.Fusion.Diagnostics; -using HotChocolate.Fusion.Execution; -using HotChocolate.Fusion.Execution.Nodes; -using HotChocolate.Utilities; +ThreadPool.SetMinThreads(1024, 1024); var builder = WebApplication.CreateBuilder(args); -builder.Services.AddHttpClient("Fusion"); +builder.Services.AddHttpClient("Fusion") + .ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler + { + MaxConnectionsPerServer = 256, + EnableMultipleHttp2Connections = true + }); builder .AddGraphQLGateway() - .AddFileSystemConfiguration("./gateway.far") - .ModifyRequestOptions(o => - { - o.CollectOperationPlanTelemetry = true; - o.IncludeExceptionDetails = true; - }) - .AddDiagnosticEventListener(_ => new ErrorCollector()); + .ModifyPlannerOptions(o => o.EnableRequestGrouping = true) + .AddFileSystemConfiguration("./gateway.far"); var app = builder.Build(); -// Start memory pool monitoring -// var memoryPoolCollector = new MemoryPoolCollector(); -// var metaDbCollector = new MetaDbCollector(); - -#if RELEASE app.MapGraphQLHttp(); -#else -app.MapGraphQL().WithOptions(new GraphQLServerOptions { Tool = { ServeMode = GraphQLToolServeMode.Insider } }); -#endif app.Run(); - -public class ErrorCollector : FusionExecutionDiagnosticEventListener -{ - private readonly ChannelWriter _writer; - - public ErrorCollector() - { - var channel = Channel.CreateUnbounded(); - _writer = channel.Writer; - - WriteErrorsAsync(channel.Reader).FireAndForget(); - } - - private async Task WriteErrorsAsync(ChannelReader reader) - { - await using var stream = File.Create("/Users/michael/local/hc-4/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Gateway/error.log"); - await using var errorLog = new StreamWriter(stream); - - await foreach (var error in reader.ReadAllAsync()) - { - await errorLog.WriteLineAsync("-------"); - await errorLog.WriteLineAsync(error.Area); - await errorLog.WriteLineAsync(error.Message); - - if (error.StackTrace is not null) - { - await errorLog.WriteLineAsync(error.StackTrace); - } - - await errorLog.FlushAsync(); - } - } - - public override void RequestError(RequestContext context, Exception error) - { - _writer.TryWrite(new ErrorInfo("RequestError", error.Message, error.StackTrace)); - } - - public override void RequestError(RequestContext context, IError error) - { - _writer.TryWrite(new ErrorInfo("RequestError", error.Message, error.Exception?.StackTrace)); - } - - public override void ValidationErrors(RequestContext context, IReadOnlyList errors) - { - foreach (var error in errors) - { - _writer.TryWrite(new ErrorInfo("ValidationError", error.Message, error.Exception?.StackTrace)); - } - } - - public override void ExecutionNodeError(OperationPlanContext context, ExecutionNode node, Exception error) - { - _writer.TryWrite(new ErrorInfo($"ExecutionNodeError (Node: {node.Id})", error.Message, error.StackTrace)); - } - - public override void PlanOperationError(RequestContext context, string operationId, Exception error) - { - _writer.TryWrite(new ErrorInfo($"PlanOperationError (Operation: {operationId})", error.Message, error.StackTrace)); - } - - public override void SourceSchemaStoreError(OperationPlanContext context, ExecutionNode node, string schemaName, Exception error) - { - _writer.TryWrite(new ErrorInfo($"SourceSchemaStoreError (Schema: {schemaName}, Node: {node.Id})", error.Message, error.StackTrace)); - } - - public override void SourceSchemaResultError(OperationPlanContext context, ExecutionNode node, string schemaName, IReadOnlyList errors) - { - foreach (var error in errors) - { - _writer.TryWrite(new ErrorInfo($"SourceSchemaResultError (Schema: {schemaName}, Node: {node.Id})", error.Message, error.Exception?.StackTrace)); - } - } - - public override void SourceSchemaTransportError(OperationPlanContext context, ExecutionNode node, string schemaName, Exception error) - { - _writer.TryWrite(new ErrorInfo($"SourceSchemaTransportError (Schema: {schemaName}, Node: {node.Id})", error.Message, error.StackTrace)); - } - - public record ErrorInfo(string Area, string Message, string? StackTrace = null); -} - -public class MemoryPoolCollector : EventListener -{ - private readonly ChannelWriter _writer; - - public MemoryPoolCollector() - { - var channel = Channel.CreateUnbounded(); - _writer = channel.Writer; - - WriteEventsAsync(channel.Reader).FireAndForget(); - } - - protected override void OnEventSourceCreated(EventSource eventSource) - { - if (eventSource.Name == "HotChocolate-Fusion-FixedSizeArrayPool") - { - // Subscribe to all events at Verbose level to capture all buffer operations - EnableEvents(eventSource, EventLevel.Verbose); - } - } - - protected override void OnEventWritten(EventWrittenEventArgs eventData) - { - var eventInfo = new MemoryEventInfo( - Timestamp: DateTime.UtcNow, - EventName: eventData.EventName ?? "Unknown", - EventId: eventData.EventId, - Level: eventData.Level.ToString(), - Message: FormatMessage(eventData), - Payload: FormatPayload(eventData) - ); - - _writer.TryWrite(eventInfo); - } - - private string FormatMessage(EventWrittenEventArgs eventData) - { - if (eventData.Message == null || eventData.Payload == null) - { - return eventData.EventName ?? "Unknown"; - } - - try - { - return string.Format(eventData.Message, eventData.Payload.ToArray()); - } - catch - { - return eventData.EventName ?? "Unknown"; - } - } - - private string FormatPayload(EventWrittenEventArgs eventData) - { - if (eventData.PayloadNames == null || eventData.Payload == null) - { - return string.Empty; - } - - var parts = new List(); - for (var i = 0; i < eventData.PayloadNames.Count && i < eventData.Payload.Count; i++) - { - parts.Add($"{eventData.PayloadNames[i]}={eventData.Payload[i]}"); - } - - return string.Join(", ", parts); - } - - private async Task WriteEventsAsync(ChannelReader reader) - { - await using var stream = File.Create("/Users/michael/local/hc-4/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Gateway/memory.log"); - await using var log = new StreamWriter(stream); - - await foreach (var ev in reader.ReadAllAsync()) - { - await log.WriteLineAsync("-------"); - await log.WriteLineAsync($"[{ev.Timestamp:O}] {ev.EventName} (ID: {ev.EventId}, Level: {ev.Level})"); - await log.WriteLineAsync($"Message: {ev.Message}"); - - if (!string.IsNullOrEmpty(ev.Payload)) - { - await log.WriteLineAsync($"Payload: {ev.Payload}"); - } - - await log.FlushAsync(); - } - } - - public record MemoryEventInfo( - DateTime Timestamp, - string EventName, - int EventId, - string Level, - string Message, - string Payload); -} - -public class MetaDbCollector : EventListener -{ - private readonly ChannelWriter _writer; - private readonly System.Collections.Concurrent.ConcurrentDictionary _dbStats = new(); - private DateTime _lastEventTime = DateTime.UtcNow; - private readonly Timer _checkTimer; - private readonly object _checkLock = new(); - private bool _reportWritten; - - public MetaDbCollector() - { - var channel = Channel.CreateUnbounded(); - _writer = channel.Writer; - - WriteEventsAsync(channel.Reader).FireAndForget(); - - // Check every 5 seconds if traffic has died down (5000ms initial delay, 5000ms period) - _checkTimer = new Timer(CheckForLeaks, null, 5000, 5000); - } - - private class DbStats - { - public int Created { get; init; } - public int Disposed { get; init; } - } - - protected override void OnEventSourceCreated(EventSource eventSource) - { - if (eventSource.Name == "HotChocolate-Fusion-MetaDb") - { - // Subscribe to all events at Verbose level to capture all MetaDb operations - EnableEvents(eventSource, EventLevel.Verbose); - } - } - - protected override void OnEventWritten(EventWrittenEventArgs eventData) - { - var eventInfo = new MetaDbEventInfo( - Timestamp: DateTime.UtcNow, - EventName: eventData.EventName ?? "Unknown", - EventId: eventData.EventId, - Level: eventData.Level.ToString(), - Message: FormatMessage(eventData), - Payload: FormatPayload(eventData) - ); - - _writer.TryWrite(eventInfo); - - // Track creates and disposes per dbId - if (eventData.EventId == 1 && eventData.Payload?.Count > 0) // MetaDbCreated - { - var dbId = Convert.ToInt32(eventData.Payload[0]); - _dbStats.AddOrUpdate(dbId, - _ => new DbStats { Created = 1, Disposed = 0 }, - (_, stats) => new DbStats { Created = stats.Created + 1, Disposed = stats.Disposed }); - _lastEventTime = DateTime.UtcNow; - _reportWritten = false; // Reset flag when new activity happens - } - else if (eventData.EventId == 2 && eventData.Payload?.Count > 0) // MetaDbDisposed - { - var dbId = Convert.ToInt32(eventData.Payload[0]); - _dbStats.AddOrUpdate(dbId, - _ => new DbStats { Created = 0, Disposed = 1 }, - (_, stats) => new DbStats { Created = stats.Created, Disposed = stats.Disposed + 1 }); - _lastEventTime = DateTime.UtcNow; - _reportWritten = false; // Reset flag when new activity happens - } - } - - private string FormatMessage(EventWrittenEventArgs eventData) - { - if (eventData.Message == null || eventData.Payload == null) - { - return eventData.EventName ?? "Unknown"; - } - - try - { - return string.Format(eventData.Message, eventData.Payload.ToArray()); - } - catch - { - return eventData.EventName ?? "Unknown"; - } - } - - private string FormatPayload(EventWrittenEventArgs eventData) - { - if (eventData.PayloadNames == null || eventData.Payload == null) - { - return string.Empty; - } - - var parts = new List(); - for (var i = 0; i < eventData.PayloadNames.Count && i < eventData.Payload.Count; i++) - { - parts.Add($"{eventData.PayloadNames[i]}={eventData.Payload[i]}"); - } - - return string.Join(", ", parts); - } - - private async Task WriteEventsAsync(ChannelReader reader) - { - await using var stream = File.Create("/Users/michael/local/hc-4/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Gateway/metadb.log"); - await using var log = new StreamWriter(stream); - - await foreach (var ev in reader.ReadAllAsync()) - { - await log.WriteLineAsync("-------"); - await log.WriteLineAsync($"[{ev.Timestamp:O}] {ev.EventName} (ID: {ev.EventId}, Level: {ev.Level})"); - await log.WriteLineAsync($"Message: {ev.Message}"); - - if (!string.IsNullOrEmpty(ev.Payload)) - { - await log.WriteLineAsync($"Payload: {ev.Payload}"); - } - - await log.FlushAsync(); - } - } - - private void CheckForLeaks(object? state) - { - lock (_checkLock) - { - // If no events for 10 seconds and we haven't written a report yet - var timeSinceLastEvent = DateTime.UtcNow - _lastEventTime; - if (timeSinceLastEvent.TotalSeconds >= 10 && !_reportWritten && _dbStats.Count > 0) - { - WriteLeakReport(); - _reportWritten = true; - } - } - } - - private void WriteLeakReport() - { - try - { - using var stream = File.Create("/Users/michael/local/hc-4/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Gateway/metadb-check.log"); - using var log = new StreamWriter(stream); - - log.WriteLine("==========================================="); - log.WriteLine("MetaDb Leak Detection Report"); - log.WriteLine($"Generated: {DateTime.UtcNow:O}"); - log.WriteLine("==========================================="); - log.WriteLine(); - - var totalCreated = 0; - var totalDisposed = 0; - var leaksDetected = new List<(int dbId, int leaked)>(); - - foreach (var kvp in _dbStats.OrderBy(x => x.Key)) - { - var dbId = kvp.Key; - var stats = kvp.Value; - var leaked = stats.Created - stats.Disposed; - - totalCreated += stats.Created; - totalDisposed += stats.Disposed; - - log.WriteLine($"DbId: {dbId}"); - log.WriteLine($" Created: {stats.Created}"); - log.WriteLine($" Disposed: {stats.Disposed}"); - log.WriteLine($" Balance: {leaked} {(leaked > 0 ? "⚠️ LEAKED" : leaked < 0 ? "⚠️ OVER-DISPOSED" : "✓ OK")}"); - log.WriteLine(); - - if (leaked != 0) - { - leaksDetected.Add((dbId, leaked)); - } - } - - log.WriteLine("==========================================="); - log.WriteLine("SUMMARY"); - log.WriteLine("==========================================="); - log.WriteLine($"Total MetaDb Created: {totalCreated}"); - log.WriteLine($"Total MetaDb Disposed: {totalDisposed}"); - log.WriteLine($"Net Leaked: {totalCreated - totalDisposed}"); - log.WriteLine(); - - if (leaksDetected.Count > 0) - { - log.WriteLine($"⚠️ {leaksDetected.Count} DbId(s) with leaks detected:"); - foreach (var (dbId, leaked) in leaksDetected) - { - log.WriteLine($" DbId {dbId}: {Math.Abs(leaked)} {(leaked > 0 ? "leaked" : "over-disposed")}"); - } - } - else - { - log.WriteLine("✓ No leaks detected - all MetaDbs properly disposed!"); - } - - log.WriteLine("==========================================="); - log.Flush(); - } - catch (Exception ex) - { - Console.WriteLine($"Failed to write leak report: {ex.Message}"); - } - } - - public record MetaDbEventInfo( - DateTime Timestamp, - string EventName, - int EventId, - string Level, - string Message, - string Payload); -} diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Inventory/Program.cs b/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Inventory/Program.cs index 25e63b69e8f..006115a9797 100644 --- a/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Inventory/Program.cs +++ b/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Inventory/Program.cs @@ -5,7 +5,7 @@ var builder = WebApplication.CreateBuilder(args); builder - .AddGraphQL("inventory-api") + .AddGraphQL("inventory-api", disableDefaultSecurity: true) .AddInventoryTypes(); var app = builder.Build(); diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Products/Program.cs b/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Products/Program.cs index f9d26e34c4d..86319e37b1f 100644 --- a/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Products/Program.cs +++ b/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Products/Program.cs @@ -5,7 +5,7 @@ var builder = WebApplication.CreateBuilder(args); builder - .AddGraphQL("products-api") + .AddGraphQL("products-api", disableDefaultSecurity: true) .AddProductTypes(); var app = builder.Build(); diff --git a/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Reviews/Program.cs b/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Reviews/Program.cs index cf1d616f90d..0384bcea533 100644 --- a/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Reviews/Program.cs +++ b/src/HotChocolate/Fusion-vnext/benchmarks/k6/eShop.Reviews/Program.cs @@ -5,7 +5,7 @@ var builder = WebApplication.CreateBuilder(args); builder.Services - .AddGraphQLServer("reviews-api") + .AddGraphQLServer("reviews-api", disableDefaultSecurity: true) .AddReviewTypes(); var app = builder.Build(); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/AggregateFusionExecutionDiagnosticEvents.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/AggregateFusionExecutionDiagnosticEvents.cs index 3435cbc6a86..f13aee78f07 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/AggregateFusionExecutionDiagnosticEvents.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/AggregateFusionExecutionDiagnosticEvents.cs @@ -184,6 +184,21 @@ public IDisposable ExecuteOperationNode( return new AggregateActivityScope(scopes); } + public IDisposable ExecuteOperationBatchNode( + OperationPlanContext context, + ExecutionNode node, + string schemaName) + { + var scopes = new IDisposable[listeners.Length]; + + for (var i = 0; i < listeners.Length; i++) + { + scopes[i] = listeners[i].ExecuteOperationBatchNode(context, node, schemaName); + } + + return new AggregateActivityScope(scopes); + } + public void SourceSchemaTransportError( OperationPlanContext context, ExecutionNode node, @@ -242,7 +257,7 @@ public IDisposable ExecuteSubscription(RequestContext context, ulong subscriptio public IDisposable ExecuteSubscriptionNode( OperationPlanContext context, - OperationExecutionNode node, + ExecutionNode node, string schemaName, ulong subscriptionId) { diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/FusionExecutionDiagnosticEventListener.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/FusionExecutionDiagnosticEventListener.cs index 4c6c7f2932b..f0cfc6e9bb6 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/FusionExecutionDiagnosticEventListener.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/FusionExecutionDiagnosticEventListener.cs @@ -93,6 +93,13 @@ public virtual IDisposable ExecuteOperationNode( string schemaName) => EmptyScope; + /// + public virtual IDisposable ExecuteOperationBatchNode( + OperationPlanContext context, + ExecutionNode node, + string schemaName) + => EmptyScope; + /// public virtual void SourceSchemaTransportError( OperationPlanContext context, @@ -133,7 +140,7 @@ public virtual IDisposable ExecuteSubscription(RequestContext context, ulong sub public virtual IDisposable ExecuteSubscriptionNode( OperationPlanContext context, - OperationExecutionNode node, + ExecutionNode node, string schemaName, ulong subscriptionId) => EmptyScope; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/IFusionExecutionDiagnosticEvents.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/IFusionExecutionDiagnosticEvents.cs index 9502f01fede..e27c5c5e2b0 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/IFusionExecutionDiagnosticEvents.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Diagnostics/IFusionExecutionDiagnosticEvents.cs @@ -113,6 +113,26 @@ IDisposable ExecuteOperationNode( OperationExecutionNode node, string schemaName); + /// + /// Called when executing an operation plan node that batches or deduplicates source schema requests. + /// + /// + /// The operation plan context. + /// + /// + /// The batch or deduplicated execution node being executed. + /// + /// + /// The name of the source schema being queried. + /// + /// + /// Returns a scope that is disposed when the node execution is completed. + /// + IDisposable ExecuteOperationBatchNode( + OperationPlanContext context, + ExecutionNode node, + string schemaName); + /// /// Called when executing an operation plan node that subscribes to a source schema subscription. /// @@ -133,7 +153,7 @@ IDisposable ExecuteOperationNode( /// IDisposable ExecuteSubscriptionNode( OperationPlanContext context, - OperationExecutionNode node, + ExecutionNode node, string schemaName, ulong subscriptionId); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/ISourceSchemaClient.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/ISourceSchemaClient.cs index 99a4c4b3e94..b126206fb50 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/ISourceSchemaClient.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/ISourceSchemaClient.cs @@ -1,12 +1,43 @@ -using HotChocolate.Fusion.Execution.Nodes; +using System.Collections.Immutable; namespace HotChocolate.Fusion.Execution.Clients; +/// +/// Represents a transport-level client for a single source schema. +/// Implementations handle the wire protocol (e.g. HTTP, WebSocket) for +/// sending GraphQL operations to a downstream service. +/// public interface ISourceSchemaClient : IAsyncDisposable { + /// + /// Gets the capabilities of this client + /// + SourceSchemaClientCapabilities Capabilities { get; } + + /// + /// Executes a single GraphQL operation against the source schema. + /// + /// The current operation plan execution context. + /// The request to execute. + /// A token to cancel the operation. + /// The response from the source schema. ValueTask ExecuteAsync( OperationPlanContext context, - ExecutionNode node, SourceSchemaClientRequest request, CancellationToken cancellationToken); + + /// + /// Executes multiple GraphQL operations as a single batched transport request. + /// + /// The current operation plan execution context. + /// The requests to include in the batch. + /// A token to cancel the operation. + /// + /// A dictionary mapping each request's ID + /// to its corresponding response. + /// + ValueTask> ExecuteBatchAsync( + OperationPlanContext context, + ImmutableArray requests, + CancellationToken cancellationToken); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/ISourceSchemaDispatcher.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/ISourceSchemaDispatcher.cs new file mode 100644 index 00000000000..0a145c41d2f --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/ISourceSchemaDispatcher.cs @@ -0,0 +1,43 @@ +namespace HotChocolate.Fusion.Execution.Clients; + +/// +/// Controls the lifecycle of batching groups during plan execution. +/// The executor uses this interface to register groups discovered in the plan, +/// notify the dispatcher when nodes are skipped, and abort all pending work +/// on cancellation or failure. +/// +public interface ISourceSchemaDispatcher +{ + /// + /// Registers a batching group. The dispatcher will hold requests for the specified + /// node IDs until all members have submitted or been skipped. + /// + /// The batching group identifier assigned at planning time. + /// The execution node IDs that belong to this group. + void RegisterGroup(int groupId, IReadOnlyList nodeIds); + + /// + /// Marks a node as skipped, removing it from its group's outstanding member count. + /// If this was the last outstanding member, the group is dispatched with + /// whatever requests have been submitted so far. + /// + /// The ID of the execution node to skip. + void SkipNode(int nodeId); + + /// + /// Aborts all pending batching groups, faulting any waiting callers with the + /// specified error. Subsequent calls to + /// and become no-ops. + /// + /// + /// The exception to propagate to pending callers, or null to use a + /// default . + /// + void Abort(Exception? error = null); + + /// + /// Resets the dispatcher to its initial state, clearing all groups and the aborted flag. + /// Must be called between subscription events so that groups can be re-registered. + /// + void Reset(); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/ISourceSchemaScheduler.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/ISourceSchemaScheduler.cs new file mode 100644 index 00000000000..9ee2b5b3cc0 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/ISourceSchemaScheduler.cs @@ -0,0 +1,22 @@ +namespace HotChocolate.Fusion.Execution.Clients; + +/// +/// Schedules the execution of source schema requests. +/// Execution nodes call this interface instead of directly, +/// allowing the scheduler to hold requests that belong to the same batching group until all +/// group members have submitted or been skipped, and then dispatch them as a single batch. +/// +public interface ISourceSchemaScheduler +{ + /// + /// Submits a request for execution. If the request belongs to a batching group, + /// the returned task may not complete until all other members of the group have + /// submitted or been skipped. + /// + /// The request to execute. + /// A token to cancel the operation. + /// The response from the source schema. + ValueTask ExecuteAsync( + SourceSchemaClientRequest request, + CancellationToken cancellationToken); +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaClientCapabilities.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaClientCapabilities.cs new file mode 100644 index 00000000000..9553654e0e6 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaClientCapabilities.cs @@ -0,0 +1,35 @@ +namespace HotChocolate.Fusion.Execution.Clients; + +/// +/// Describes the transport-level capabilities that a +/// supports. Because this is a enum, values can be combined +/// to advertise multiple capabilities at once. +/// +[Flags] +public enum SourceSchemaClientCapabilities +{ + /// + /// The client supports variable batching, where a single request carries multiple + /// sets of variables so the downstream service can resolve them in one round-trip. + /// + VariableBatching = 1, + + /// + /// The client supports request batching, where multiple independent GraphQL + /// operations are sent as an array in a single HTTP request. + /// + RequestBatching = 2, + + /// + /// The client supports the Apollo-style request batching format, which uses a + /// JSON array of operation objects as the request body. + /// + ApolloRequestBatching = 4, + + /// + /// The client supports file upload via the + /// + /// GraphQL multipart request specification. + /// + FileUpload = 8 +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaClientRequest.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaClientRequest.cs index 4238d6f6eaa..54035737ac8 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaClientRequest.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaClientRequest.cs @@ -1,15 +1,51 @@ using System.Collections.Immutable; +using HotChocolate.Fusion.Execution.Nodes; using HotChocolate.Language; namespace HotChocolate.Fusion.Execution.Clients; +/// +/// Describes a single GraphQL request to be sent to a source schema. +/// public sealed class SourceSchemaClientRequest { + /// + /// Gets the execution node that produced this request. + /// + public required ExecutionNode Node { get; init; } + + /// + /// Gets the name of the source schema this request targets. + /// + public required string SchemaName { get; init; } + + /// + /// Gets the optional batching group identifier assigned at planning time. + /// When set, the holds this request until + /// all nodes in the same group have submitted or been skipped, then dispatches + /// them together via . + /// + public int? BatchingGroupId { get; init; } + + /// + /// Gets the GraphQL operation type (query, mutation, or subscription). + /// public required OperationType OperationType { get; init; } + /// + /// Gets the GraphQL operation source text to send. + /// public required string OperationSourceText { get; init; } + /// + /// Gets the variable value sets for this operation. Multiple entries indicate + /// that the operation should be executed once per variable set (variable batching). + /// public ImmutableArray Variables { get; init; } = []; + /// + /// Gets whether the operation contains variables that include the Upload scalar, + /// requiring multipart form encoding. + /// public bool RequiresFileUpload { get; init; } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaClientResponse.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaClientResponse.cs index d90016844d0..eb48d8ce2d8 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaClientResponse.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaClientResponse.cs @@ -1,15 +1,42 @@ namespace HotChocolate.Fusion.Execution.Clients; +/// +/// Represents the response received from a source schema after executing a GraphQL operation. +/// The response carries transport metadata and provides access to the result stream. +/// public abstract class SourceSchemaClientResponse : IDisposable { + /// + /// Gets the URI that the request was sent to. + /// public abstract Uri Uri { get; } + /// + /// Gets the content type of the response (e.g. application/json). + /// public abstract string ContentType { get; } + /// + /// Gets whether the transport-level response indicates success (e.g. HTTP 2xx). + /// public abstract bool IsSuccessful { get; } + /// + /// Reads the response as an asynchronous stream of items. + /// For non-batched responses this yields a single result; for batched or streamed responses + /// it may yield multiple results. + /// + /// + /// A token to cancel reading. + /// + /// + /// An async enumerable of source schema results. + /// public abstract IAsyncEnumerable ReadAsResultStreamAsync( CancellationToken cancellationToken = default); + /// + /// Releases transport resources held by this response. + /// public abstract void Dispose(); } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs index 795c3fa395f..85355aec046 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClient.cs @@ -1,7 +1,11 @@ +using System.Collections.Concurrent; using System.Collections.Immutable; +using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; using System.Text.Json; using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Fusion.Properties; using HotChocolate.Fusion.Text.Json; using HotChocolate.Fusion.Transport.Http; using HotChocolate.Language; @@ -9,14 +13,25 @@ namespace HotChocolate.Fusion.Execution.Clients; +/// +/// HTTP-based implementation of that sends GraphQL operations +/// to a downstream service over HTTP. Supports single requests, Apollo-style request batching, +/// and variable batching depending on the configured . +/// public sealed class SourceSchemaHttpClient : ISourceSchemaClient { private static ReadOnlySpan VariableIndex => "variableIndex"u8; + private static ReadOnlySpan RequestIndex => "requestIndex"u8; private readonly GraphQLHttpClient _client; private readonly SourceSchemaHttpClientConfiguration _configuration; private bool _disposed; + /// + /// Initializes a new instance of . + /// + /// The underlying HTTP client used to send requests. + /// The transport configuration for this source schema. public SourceSchemaHttpClient( GraphQLHttpClient client, SourceSchemaHttpClientConfiguration configuration) @@ -26,31 +41,43 @@ public SourceSchemaHttpClient( _client = client; _configuration = configuration; + + var capabilities = SourceSchemaClientCapabilities.FileUpload; + + if (configuration.BatchingMode.HasFlag(SourceSchemaHttpClientBatchingMode.VariableBatching)) + { + capabilities |= SourceSchemaClientCapabilities.VariableBatching; + } + + if (configuration.BatchingMode.HasFlag(SourceSchemaHttpClientBatchingMode.RequestBatching)) + { + capabilities |= SourceSchemaClientCapabilities.RequestBatching; + } + + if (configuration.BatchingMode.HasFlag(SourceSchemaHttpClientBatchingMode.ApolloRequestBatching)) + { + capabilities |= SourceSchemaClientCapabilities.ApolloRequestBatching; + } + + Capabilities = capabilities; } + /// + public SourceSchemaClientCapabilities Capabilities { get; } + + /// public async ValueTask ExecuteAsync( OperationPlanContext context, - ExecutionNode node, SourceSchemaClientRequest request, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(context); ArgumentNullException.ThrowIfNull(request); - var httpRequest = CreateHttpRequest(request); - httpRequest.State = (context, node, _configuration); - - httpRequest.OnMessageCreated += static (_, requestMessage, state) => - { - var (context, node, configuration) = ((OperationPlanContext, ExecutionNode, SourceSchemaHttpClientConfiguration))state!; - configuration.OnBeforeSend?.Invoke(context, node, requestMessage); - }; + Debug.WriteLine(request.SchemaName); - httpRequest.OnMessageReceived += static (_, responseMessage, state) => - { - var (context, node, configuration) = ((OperationPlanContext, ExecutionNode, SourceSchemaHttpClientConfiguration))state!; - configuration.OnAfterReceive?.Invoke(context, node, responseMessage); - }; + var httpRequest = CreateHttpRequest(request); + ConfigureCallbacks(httpRequest, context, request); var httpResponse = await _client.SendAsync(httpRequest, cancellationToken); return new Response( @@ -60,6 +87,61 @@ public async ValueTask ExecuteAsync( request.Variables); } + /// + public async ValueTask> ExecuteBatchAsync( + OperationPlanContext context, + ImmutableArray requests, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(context); + + if (requests.Length == 0) + { + return []; + } + + Debug.WriteLine(requests[0].SchemaName); + + if (ContainsSubscriptionRequest(requests)) + { + throw new InvalidOperationException( + FusionExecutionResources.SourceSchemaHttpClient_SubscriptionBatchNotSupported); + } + + var httpRequest = CreateHttpBatchRequest(requests); + ConfigureBatchCallbacks(httpRequest, context, requests); + + var httpResponse = await _client.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false); + + var uri = httpRequest.Uri ?? new Uri("http://unknown"); + var contentType = httpResponse.ContentHeaders.ContentType?.ToString() ?? "unknown"; + var isSuccessful = httpResponse.IsSuccessStatusCode; + + var nodeResponsesByNodeId = new Dictionary(requests.Length); + var builder = ImmutableArray.CreateBuilder(requests.Length); + + for (var i = 0; i < requests.Length; i++) + { + var nodeResponse = new NodeResponse(uri, contentType, isSuccessful); + nodeResponsesByNodeId[requests[i].Node.Id] = nodeResponse; + builder.Add(nodeResponse); + } + + _ = ReadBatchStreamInBackgroundAsync( + context, + requests, + nodeResponsesByNodeId, + httpResponse, + cancellationToken); + + return builder.MoveToImmutable(); + } + + /// + /// Creates the appropriate for the given request, + /// choosing between a single operation, an Apollo operation batch, or a variable batch + /// based on the number of variable sets and the configured batching mode. + /// private GraphQLHttpRequest CreateHttpRequest( SourceSchemaClientRequest originalRequest) { @@ -106,6 +188,56 @@ private GraphQLHttpRequest CreateHttpRequest( } } + private GraphQLHttpRequest CreateHttpBatchRequest( + IReadOnlyList originalRequests) + { + var batchRequests = new List(originalRequests.Count); + var enableFileUploads = false; + + for (var i = 0; i < originalRequests.Count; i++) + { + var sourceRequest = originalRequests[i]; + enableFileUploads |= sourceRequest.RequiresFileUpload; + + var body = CreateRequestBody(sourceRequest); + if (body is IOperationRequest operationRequest) + { + batchRequests.Add(operationRequest); + } + else + { + throw new InvalidOperationException( + $"The request body type '{body.GetType().Name}' cannot be included in an operation batch."); + } + } + + return new GraphQLHttpRequest(new OperationBatchRequest(batchRequests)) + { + Uri = _configuration.BaseAddress, + Accept = _configuration.BatchingAcceptHeaderValues, + EnableFileUploads = enableFileUploads + }; + } + + private static IRequestBody CreateRequestBody( + SourceSchemaClientRequest originalRequest) + { + var operationSourceText = originalRequest.OperationSourceText; + + switch (originalRequest.Variables.Length) + { + case 0: + return CreateSingleRequest(operationSourceText); + + case 1: + var variableValues = originalRequest.Variables[0].Values; + return CreateSingleRequest(operationSourceText, variableValues); + + default: + return CreateVariableBatchRequest(operationSourceText, originalRequest); + } + } + private static OperationRequest CreateSingleRequest( string operationSourceText, ObjectValueNode? variables = null) @@ -155,6 +287,135 @@ private static VariableBatchRequest CreateVariableBatchRequest( extensions: null); } + private async Task ReadBatchStreamInBackgroundAsync( + OperationPlanContext context, + ImmutableArray requests, + Dictionary nodeResponses, + GraphQLHttpResponse httpResponse, + CancellationToken cancellationToken) + { + try + { + await foreach (var result in httpResponse.ReadAsResultStreamAsync() + .WithCancellation(cancellationToken)) + { + var requestIndex = result.Root.GetProperty(RequestIndex).GetInt32(); + + if ((uint)requestIndex >= (uint)requests.Length) + { + result.Dispose(); + throw new InvalidOperationException( + string.Format( + FusionExecutionResources.SourceSchemaHttpClient_InvalidRequestIndex, + requestIndex)); + } + + var request = requests[requestIndex]; + + if (!nodeResponses.TryGetValue(request.Node.Id, out var nodeResponse)) + { + result.Dispose(); + throw new InvalidOperationException( + string.Format( + FusionExecutionResources.SourceSchemaHttpClient_NoResponseChannelForNode, + request.Node.Id)); + } + + var variableIndex = ResolveVariableIndex(request, result); + + if (!TryGetResultPath(request, variableIndex, out var path, out var additionalPaths)) + { + result.Dispose(); + throw new InvalidOperationException( + string.Format( + FusionExecutionResources.SourceSchemaHttpClient_InvalidVariableIndex, + variableIndex, + request.Node.Id)); + } + + WriteResultToChannel(context, request.Node, nodeResponse, path, additionalPaths, result); + } + + // Stream completed successfully. Complete all channels, failing any + // that never received results (fail-loud). + foreach (var (nodeId, nodeResponse) in nodeResponses) + { + if (!nodeResponse.HasReceivedResults) + { + nodeResponse.Complete( + new InvalidOperationException( + string.Format( + FusionExecutionResources.SourceSchemaHttpClient_NoResultForNode, + nodeId))); + } + else + { + nodeResponse.Complete(); + } + } + } + catch (Exception ex) + { + foreach (var nodeResponse in nodeResponses.Values) + { + nodeResponse.Complete(ex); + } + } + finally + { + httpResponse.Dispose(); + } + } + + private static int ResolveVariableIndex( + SourceSchemaClientRequest request, + SourceResultDocument result) + { + var variableCount = request.Variables.Length; + + if (variableCount <= 1) + { + return 0; + } + + var variableIndex = result.Root.GetProperty(VariableIndex).GetInt32(); + + if ((uint)variableIndex < (uint)variableCount) + { + return variableIndex; + } + + throw new InvalidOperationException( + $"The batch response contains an out-of-range variableIndex '{variableIndex}'."); + } + + private static bool TryGetResultPath( + SourceSchemaClientRequest request, + int variableIndex, + out Path path, + out ImmutableArray additionalPaths) + { + if (request.Variables.Length == 0) + { + path = Path.Root; + additionalPaths = []; + return true; + } + + if ((uint)variableIndex >= (uint)request.Variables.Length) + { + path = Path.Root; + additionalPaths = []; + return false; + } + + var variable = request.Variables[variableIndex]; + path = variable.Path; + additionalPaths = variable.AdditionalPaths; + return true; + } + + /// public ValueTask DisposeAsync() { if (_disposed) @@ -168,6 +429,119 @@ public ValueTask DisposeAsync() return ValueTask.CompletedTask; } + /// + /// Attaches and + /// callbacks to + /// a single HTTP request. + /// + private void ConfigureCallbacks( + GraphQLHttpRequest request, + OperationPlanContext context, + SourceSchemaClientRequest sourceRequest) + { + request.State = (context, sourceRequest.Node, _configuration); + + request.OnMessageCreated += static (_, requestMessage, state) => + { + var (context, node, configuration) = + ((OperationPlanContext, ExecutionNode, SourceSchemaHttpClientConfiguration))state!; + configuration.OnBeforeSend?.Invoke(context, node, requestMessage); + }; + + request.OnMessageReceived += static (_, responseMessage, state) => + { + var (context, node, configuration) = + ((OperationPlanContext, ExecutionNode, SourceSchemaHttpClientConfiguration))state!; + configuration.OnAfterReceive?.Invoke(context, node, responseMessage); + }; + } + + /// + /// Attaches and + /// callbacks to + /// the HTTP request, invoking them for each node in the batch. + /// + private void ConfigureBatchCallbacks( + GraphQLHttpRequest request, + OperationPlanContext context, + IReadOnlyList requests) + { + request.State = (context, requests, _configuration); + + request.OnMessageCreated += static (_, requestMessage, state) => + { + var (context, requests, configuration) = + ((OperationPlanContext, IReadOnlyList, SourceSchemaHttpClientConfiguration))state!; + + for (var i = 0; i < requests.Count; i++) + { + configuration.OnBeforeSend?.Invoke(context, requests[i].Node, requestMessage); + } + }; + + request.OnMessageReceived += static (_, responseMessage, state) => + { + var (context, requests, configuration) = + ((OperationPlanContext, IReadOnlyList, SourceSchemaHttpClientConfiguration))state!; + + for (var i = 0; i < requests.Count; i++) + { + configuration.OnAfterReceive?.Invoke(context, requests[i].Node, responseMessage); + } + }; + } + + private void WriteResultToChannel( + OperationPlanContext context, + ExecutionNode node, + NodeResponse nodeResponse, + Path path, + ImmutableArray additionalPaths, + SourceResultDocument document) + { + var sourceSchemaResult = new SourceSchemaResult(path, document); + _configuration.OnSourceSchemaResult?.Invoke(context, node, sourceSchemaResult); + + if (!nodeResponse.TryWrite(sourceSchemaResult)) + { + sourceSchemaResult.Dispose(); + return; + } + + nodeResponse.HasReceivedResults = true; + + foreach (var additionalPath in additionalPaths) + { + var alias = sourceSchemaResult.WithPath(additionalPath); + _configuration.OnSourceSchemaResult?.Invoke(context, node, alias); + + if (!nodeResponse.TryWrite(alias)) + { + // alias does not own the document (ownsDocument: false via WithPath), + // so no disposal needed for the alias itself. + return; + } + } + } + + private static bool ContainsSubscriptionRequest( + IReadOnlyList requests) + { + for (var i = 0; i < requests.Count; i++) + { + if (requests[i].OperationType is OperationType.Subscription) + { + return true; + } + } + + return false; + } + + /// + /// A live response backed by an in-flight HTTP response. Used for single (non-batched) + /// requests where the response stream is read lazily on enumeration. + /// private sealed class Response( OperationType operation, GraphQLHttpRequest request, @@ -175,6 +549,12 @@ private sealed class Response( ImmutableArray variables) : SourceSchemaClientResponse { + public override Uri Uri => request.Uri ?? new Uri("http://unknown"); + + public override string ContentType => response.ContentHeaders.ContentType?.ToString() ?? "unknown"; + + public override bool IsSuccessful => response.IsSuccessStatusCode; + public override async IAsyncEnumerable ReadAsResultStreamAsync( [EnumeratorCancellation] CancellationToken cancellationToken = default) { @@ -237,6 +617,13 @@ public override async IAsyncEnumerable ReadAsResultStreamAsy await foreach (var result in response.ReadAsResultStreamAsync() .WithCancellation(cancellationToken)) { + if ((uint)requestIndex >= (uint)variables.Length) + { + errorResult = new SourceSchemaResult(variables[0].Path, result); + configuration.OnSourceSchemaResult?.Invoke(context, node, errorResult); + break; + } + var variable = variables[requestIndex]; var sourceSchemaResult = new SourceSchemaResult(variable.Path, result); @@ -268,6 +655,14 @@ public override async IAsyncEnumerable ReadAsResultStreamAsy } var index = variableIndex.GetInt32(); + + if ((uint)index >= (uint)variables.Length) + { + errorResult = new SourceSchemaResult(variables[0].Path, result); + configuration.OnSourceSchemaResult?.Invoke(context, node, errorResult); + break; + } + var variable = variables[index]; var sourceSchemaResult = new SourceSchemaResult(variable.Path, result); @@ -318,12 +713,109 @@ public override async IAsyncEnumerable ReadAsResultStreamAsy } } - public override Uri Uri => request.Uri ?? new Uri("http://unknown"); + public override void Dispose() => response.Dispose(); + } - public override string ContentType => response.ContentHeaders.ContentType?.ToString() ?? "unknown"; + /// + /// A streaming response for a single execution node within a batched HTTP request. + /// Results are pushed into a by the background stream + /// reader and signalled via a lightweight . + /// The execution node reads lazily via . + /// + private sealed class NodeResponse : SourceSchemaClientResponse + { + private readonly ConcurrentQueue _results = new(); + private readonly AsyncAutoResetEvent _signal = new(); + private volatile bool _completed; + private Exception? _error; + private bool _disposed; - public override bool IsSuccessful => response.IsSuccessStatusCode; + public NodeResponse(Uri uri, string contentType, bool isSuccessful) + { + Uri = uri; + ContentType = contentType; + IsSuccessful = isSuccessful; + } - public override void Dispose() => response.Dispose(); + public override Uri Uri { get; } + + public override string ContentType { get; } + + public override bool IsSuccessful { get; } + + /// + /// Gets whether at least one result has been written to this response. + /// Used to detect nodes that received no results from the batch stream. + /// + internal bool HasReceivedResults { get; set; } + + internal bool TryWrite(SourceSchemaResult result) + { + if (_disposed) + { + return false; + } + + _results.Enqueue(result); + _signal.Set(); + return true; + } + + internal void Complete(Exception? error = null) + { + _error = error; + _completed = true; + _signal.Set(); + } + + public override async IAsyncEnumerable ReadAsResultStreamAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + while (true) + { + cancellationToken.ThrowIfCancellationRequested(); + + while (_results.TryDequeue(out var result)) + { + yield return result; + } + + if (_completed) + { + // Final drain — writer may have enqueued between our last + // TryDequeue and the completion flag becoming visible. + while (_results.TryDequeue(out var result)) + { + yield return result; + } + + if (_error is not null) + { + ExceptionDispatchInfo.Throw(_error); + } + + yield break; + } + + await _signal; + } + } + + public override void Dispose() + { + if (_disposed) + { + return; + } + + _disposed = true; + + Complete(); + + while (_results.TryDequeue(out var result)) + { + result.Dispose(); + } + } } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientBatchingMode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientBatchingMode.cs index d4610de6007..db37931065b 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientBatchingMode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientBatchingMode.cs @@ -1,7 +1,9 @@ namespace HotChocolate.Fusion.Execution.Clients; +[Flags] public enum SourceSchemaHttpClientBatchingMode { - VariableBatching, - ApolloRequestBatching + VariableBatching = 1, + RequestBatching = 2, + ApolloRequestBatching = 4 } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientConfiguration.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientConfiguration.cs index e19ea5b7cea..e8275a9b313 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientConfiguration.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaHttpClientConfiguration.cs @@ -49,7 +49,9 @@ public SourceSchemaHttpClientConfiguration( string name, Uri baseAddress, SupportedOperationType supportedOperations = SupportedOperationType.All, - SourceSchemaHttpClientBatchingMode batchingMode = SourceSchemaHttpClientBatchingMode.VariableBatching, + SourceSchemaHttpClientBatchingMode batchingMode = + SourceSchemaHttpClientBatchingMode.VariableBatching + | SourceSchemaHttpClientBatchingMode.RequestBatching, ImmutableArray? defaultAcceptHeaderValues = null, ImmutableArray? batchingAcceptHeaderValues = null, ImmutableArray? subscriptionAcceptHeaderValues = null, @@ -112,7 +114,9 @@ public SourceSchemaHttpClientConfiguration( string httpClientName, Uri baseAddress, SupportedOperationType supportedOperations = SupportedOperationType.All, - SourceSchemaHttpClientBatchingMode batchingMode = SourceSchemaHttpClientBatchingMode.VariableBatching, + SourceSchemaHttpClientBatchingMode batchingMode = + SourceSchemaHttpClientBatchingMode.VariableBatching + | SourceSchemaHttpClientBatchingMode.RequestBatching, ImmutableArray? defaultAcceptHeaderValues = null, ImmutableArray? batchingAcceptHeaderValues = null, ImmutableArray? subscriptionAcceptHeaderValues = null, @@ -132,6 +136,11 @@ public SourceSchemaHttpClientConfiguration( DefaultAcceptHeaderValues = defaultAcceptHeaderValues ?? AcceptContentTypes.Default; + if (batchingMode.HasFlag(SourceSchemaHttpClientBatchingMode.VariableBatching)) + { + batchingMode &= ~SourceSchemaHttpClientBatchingMode.ApolloRequestBatching; + } + if (batchingAcceptHeaderValues.HasValue) { BatchingAcceptHeaderValues = batchingAcceptHeaderValues.Value; @@ -207,7 +216,7 @@ public SourceSchemaHttpClientConfiguration( private static class AcceptContentTypes { - public static readonly ImmutableArray Default = + public static ImmutableArray Default { get; } = [ new("application/graphql-response+json") { CharSet = "utf-8" }, new("application/json") { CharSet = "utf-8" }, @@ -223,10 +232,8 @@ private static class AcceptContentTypes new("application/json") { CharSet = "utf-8" } ]; - public static readonly ImmutableArray ApolloRequestBatching = + public static ImmutableArray ApolloRequestBatching { get; } = [ - new("application/jsonl") { CharSet = "utf-8" }, - new("text/event-stream") { CharSet = "utf-8" }, new("application/json") { CharSet = "utf-8" } ]; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs new file mode 100644 index 00000000000..3bcf2e8f875 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Clients/SourceSchemaRequestDispatcher.cs @@ -0,0 +1,467 @@ +using System.Collections.Immutable; +using HotChocolate.Fusion.Properties; +using HotChocolate.Language; +using static HotChocolate.Fusion.Execution.Clients.SourceSchemaClientCapabilities; + +namespace HotChocolate.Fusion.Execution.Clients; + +/// +/// Coordinates the dispatch of source schema requests, implementing both +/// and . +/// +/// Requests that do not belong to a batching group (or are subscriptions) are forwarded +/// directly to the underlying . Grouped requests are +/// held until every node in the group has submitted or been skipped, at which point they +/// are dispatched together via . +/// +/// +internal sealed class SourceSchemaRequestDispatcher + : ISourceSchemaScheduler + , ISourceSchemaDispatcher +{ + private readonly object _sync = new(); + private readonly OperationPlanContext _context; + private readonly ISourceSchemaClientScope _clientScope; + private readonly CancellationToken _requestAborted; + private readonly Dictionary _groups = []; + private readonly Dictionary _groupByNodeId = []; + private Exception? _abortError; + private bool _aborted; + + /// + /// Initializes a new instance of + /// using the given to obtain the client scope and + /// cancellation token for all downstream requests. + /// + /// + /// The operation plan context that owns this dispatcher. The dispatcher uses + /// to resolve clients and + /// to propagate cancellation. + /// + public SourceSchemaRequestDispatcher(OperationPlanContext context) + { + ArgumentNullException.ThrowIfNull(context); + + _context = context; + _clientScope = context.ClientScope; + _requestAborted = context.RequestContext.RequestAborted; + } + + /// + /// Executes a source schema request. If the request belongs to a batching group, + /// it is held until all nodes in that group have submitted or been skipped, then + /// dispatched as a batch. Otherwise, it is forwarded immediately. + /// + /// The source schema request to execute. + /// A token to cancel the operation. + /// The response from the source schema. + /// + /// The request's node was not registered in the expected batching group. + /// + public ValueTask ExecuteAsync( + SourceSchemaClientRequest request, + CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(request); + + var client = _clientScope.GetClient(request.SchemaName, request.OperationType); + + // if the request is not part of a batch group, + // if it is a mutation or subscription, + // or if the source schema does not support request batching, + // we will dispatch it right away without waiting for other requests. + if ((client.Capabilities & RequestBatching) != RequestBatching + || request.BatchingGroupId is not { } groupId + || request.OperationType is OperationType.Mutation or OperationType.Subscription) + { + return client.ExecuteAsync(_context, request, cancellationToken); + } + + PendingRequest? pendingRequest = null; + ImmutableArray pendingRequests = []; + var needsDispatch = false; + Exception? abortError = null; + + lock (_sync) + { + // the execution was aborted by the operation plan executor. + if (_aborted) + { + abortError = CreateAbortException(); + } + // we register the node to be dispatched. + else if (_groups.TryGetValue(groupId, out var group) + && group.TrySubmit(request, out pendingRequest)) + { + if (group.TryCreateDispatch(out pendingRequests)) + { + needsDispatch = true; + RemoveGroup(group); + } + } + // we are in an invalid state where the executor did not announce all groups or nodes. + else + { + abortError = new InvalidOperationException( + string.Format( + FusionExecutionResources.SourceSchemaRequestDispatcher_NodeNotRegisteredInGroup, + request.Node.Id, + groupId)); + } + } + + // now we handle the decisions we made in the lock. + if (abortError is not null) + { + return ValueTask.FromException(abortError); + } + + if (needsDispatch) + { + BeginDispatchGroup(pendingRequests); + } + + return new ValueTask(pendingRequest!.Completion.Task); + } + + /// + /// Registers a batching group with the given node IDs. All registered nodes must + /// either submit a request via or be skipped via + /// before the group is dispatched. + /// + /// The batching group identifier. + /// The execution node IDs that belong to this group. + public void RegisterGroup(int groupId, IReadOnlyList nodeIds) + { + ArgumentNullException.ThrowIfNull(nodeIds); + + if (nodeIds.Count == 0) + { + throw new ArgumentException( + FusionExecutionResources.SourceSchemaRequestDispatcher_RegisterGroupEmptyNodeIds, + nameof(nodeIds)); + } + + lock (_sync) + { + if (_aborted) + { + return; + } + + if (!_groups.TryGetValue(groupId, out var group)) + { + group = new GroupState(groupId); + _groups.Add(groupId, group); + } + + group.Register(nodeIds); + + foreach (var nodeId in nodeIds) + { + _groupByNodeId[nodeId] = groupId; + } + } + } + + /// + /// Marks a node as skipped so it no longer blocks dispatch of its batching group. + /// If this was the last remaining node in the group, the group is dispatched. + /// + /// The execution node ID to skip. + public void SkipNode(int nodeId) + { + ImmutableArray pendingRequests = []; + var needsDispatch = false; + + lock (_sync) + { + if (_aborted) + { + return; + } + + if (!_groupByNodeId.TryGetValue(nodeId, out var groupId) + || !_groups.TryGetValue(groupId, out var group)) + { + return; + } + + group.Skip(nodeId); + + if (group.TryCreateDispatch(out pendingRequests)) + { + needsDispatch = true; + RemoveGroup(group); + } + } + + if (needsDispatch) + { + BeginDispatchGroup(pendingRequests); + } + } + + /// + /// Aborts the dispatcher, failing all pending requests with the given error. + /// Subsequent calls to , , + /// and become no-ops. + /// + /// + /// The error to propagate to pending requests. If null, an + /// is used. + /// + public void Abort(Exception? error = null) + { + PendingRequest[] pendingRequests; + Exception abortError; + + lock (_sync) + { + if (_aborted) + { + return; + } + + _aborted = true; + _abortError = error ?? new OperationCanceledException(FusionExecutionResources.SourceSchemaRequestDispatcher_OperationAborted); + abortError = _abortError; + pendingRequests = [.. _groups.Values.SelectMany(static t => t.PendingRequests)]; + + _groups.Clear(); + _groupByNodeId.Clear(); + } + + foreach (var pendingRequest in pendingRequests) + { + pendingRequest.Completion.TrySetException(abortError); + } + } + + /// + /// Resets the dispatcher to its initial state, clearing all groups and the aborted flag. + /// Any pending requests from a prior event are abandoned (they should have been + /// completed or aborted before calling this). + /// + public void Reset() + { + lock (_sync) + { + _aborted = false; + _abortError = null; + _groups.Clear(); + _groupByNodeId.Clear(); + } + } + + private void BeginDispatchGroup(ImmutableArray pendingRequests) + { + // if pending requests is 0 it mean the the whole group was skipped and we do not need to do anything. + if (pendingRequests.Length == 0) + { + return; + } + + // in all other cases we dispatch the group asynchronously. + _ = DispatchGroupAsync(pendingRequests); + } + + private async Task DispatchGroupAsync(ImmutableArray pendingRequests) + { + try + { + if (pendingRequests.Length == 1) + { + var pendingRequest = pendingRequests[0]; + + var client = _clientScope.GetClient( + pendingRequest.Request.SchemaName, + pendingRequest.Request.OperationType); + + await DispatchSingleAsync(client, pendingRequest).ConfigureAwait(false); + } + else + { + var client = _clientScope.GetClient( + pendingRequests[0].Request.SchemaName, + pendingRequests[0].Request.OperationType); + + await DispatchBatchAsync(client, pendingRequests).ConfigureAwait(false); + } + } + catch (Exception ex) + { + foreach (var pendingRequest in pendingRequests) + { + pendingRequest.Completion.TrySetException(ex); + } + } + } + + private async ValueTask DispatchSingleAsync( + ISourceSchemaClient client, + PendingRequest pendingRequest) + { + try + { + var response = await client.ExecuteAsync( + _context, + pendingRequest.Request, + _requestAborted) + .ConfigureAwait(false); + + if (!pendingRequest.Completion.TrySetResult(response)) + { + response.Dispose(); + } + } + catch (OperationCanceledException) + { + pendingRequest.Completion.TrySetCanceled(); + } + catch (Exception ex) + { + pendingRequest.Completion.TrySetException(ex); + } + } + + private async ValueTask DispatchBatchAsync( + ISourceSchemaClient client, + ImmutableArray pendingRequests) + { + try + { + var responses = await client.ExecuteBatchAsync( + _context, + [.. pendingRequests.Select(t => t.Request)], + _requestAborted) + .ConfigureAwait(false); + + if (responses.Length != pendingRequests.Length) + { + throw new InvalidOperationException( + FusionExecutionResources.SourceSchemaRequestDispatcher_BatchResponseCountMismatch); + } + + for (var i = 0; i < pendingRequests.Length; i++) + { + var pendingRequest = pendingRequests[i]; + var response = responses[i]; + + if (!pendingRequest.Completion.TrySetResult(response)) + { + response.Dispose(); + } + } + } + catch (OperationCanceledException) + { + foreach (var pendingRequest in pendingRequests) + { + pendingRequest.Completion.TrySetCanceled(); + } + } + catch (Exception ex) + { + foreach (var pendingRequest in pendingRequests) + { + pendingRequest.Completion.TrySetException(ex); + } + } + } + + private Exception CreateAbortException() + => _abortError ?? new OperationCanceledException(FusionExecutionResources.SourceSchemaRequestDispatcher_OperationAborted); + + private void RemoveGroup(GroupState group) + { + _groups.Remove(group.Id); + + foreach (var nodeId in group.NodeIds) + { + _groupByNodeId.Remove(nodeId); + } + } + + private sealed class GroupState(int id) + { + private readonly HashSet _nodeIds = []; + private readonly HashSet _remainingNodeIds = []; + private readonly Dictionary _pendingRequests = []; + private bool _dispatchCreated; + + public int Id { get; } = id; + + public IEnumerable NodeIds => _nodeIds; + + public IEnumerable PendingRequests => _pendingRequests.Values; + + public void Register(IReadOnlyList nodeIds) + { + foreach (var nodeId in nodeIds) + { + _nodeIds.Add(nodeId); + _remainingNodeIds.Add(nodeId); + } + } + + public bool TrySubmit( + SourceSchemaClientRequest request, + out PendingRequest? pendingRequest) + { + var nodeId = request.Node.Id; + + if (!_nodeIds.Contains(nodeId)) + { + pendingRequest = null; + return false; + } + + if (_pendingRequests.ContainsKey(nodeId)) + { + throw new InvalidOperationException( + string.Format( + FusionExecutionResources.SourceSchemaRequestDispatcher_DuplicateNodeSubmission, + nodeId)); + } + + _remainingNodeIds.Remove(nodeId); + + pendingRequest = new PendingRequest(request); + _pendingRequests.Add(nodeId, pendingRequest); + + return true; + } + + public void Skip(int nodeId) + => _remainingNodeIds.Remove(nodeId); + + public bool TryCreateDispatch(out ImmutableArray pendingRequests) + { + if (_dispatchCreated || _remainingNodeIds.Count > 0) + { + pendingRequests = []; + return false; + } + + _dispatchCreated = true; + + if (_pendingRequests.Count == 0) + { + pendingRequests = []; + return true; + } + + pendingRequests = [.. _pendingRequests.Values]; + return true; + } + } + + private sealed class PendingRequest(SourceSchemaClientRequest request) + { + public SourceSchemaClientRequest Request { get; } = request; + + public TaskCompletionSource Completion { get; } = + new(TaskCreationOptions.RunContinuationsAsynchronously); + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/ExecutionState.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/ExecutionState.cs index ab244fd1650..604b60e5cb2 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/ExecutionState.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/ExecutionState.cs @@ -82,7 +82,7 @@ public void StartNode(OperationPlanContext context, ExecutionNode node, Cancella { Interlocked.Increment(ref _activeNodes); _backlog.Remove(node); - node.ExecuteAsync(context, cancellationToken).FireAndForget(); + _ = node.ExecuteAsync(context, cancellationToken); } public void EnqueueForCompletion(ExecutionNodeResult result) @@ -102,7 +102,10 @@ public void CancelProcessing() } } - public void CompleteNode(ExecutionNode node, ExecutionNodeResult result) + public void CompleteNode( + OperationPlanContext context, + ExecutionNode node, + ExecutionNodeResult result) { Interlocked.Decrement(ref _activeNodes); @@ -138,7 +141,7 @@ public void CompleteNode(ExecutionNode node, ExecutionNodeResult result) { if (!result.DependentsToExecute.Contains(dependent)) { - SkipNode(dependent); + SkipNode(context, dependent); } } } @@ -146,17 +149,19 @@ public void CompleteNode(ExecutionNode node, ExecutionNodeResult result) if (result.Status is ExecutionStatus.Skipped or ExecutionStatus.Failed) { - SkipNode(node); + SkipNode(context, node); } } - public void SkipNode(ExecutionNode node) + public void SkipNode(OperationPlanContext context, ExecutionNode node) { _stack.Clear(); _stack.Push(node); while (_stack.TryPop(out var current)) { + context.SourceSchemaDispatcher.SkipNode(current.Id); + if (_backlog.Remove(current) && collectTelemetry && !_completed.Contains(current) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs index 152106f676b..16fd60604fa 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/FusionRequestExecutorManager.cs @@ -344,7 +344,8 @@ private static SourceSchemaHttpClientBatchingMode GetBatchingMode(JsonElement ht return SourceSchemaHttpClientBatchingMode.ApolloRequestBatching; } - return SourceSchemaHttpClientBatchingMode.VariableBatching; + return SourceSchemaHttpClientBatchingMode.VariableBatching + | SourceSchemaHttpClientBatchingMode.RequestBatching; } private FeatureCollection CreateSchemaFeatures( diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNode.cs index cfb2a2e4783..8fd0b8d9af6 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNode.cs @@ -28,6 +28,13 @@ public abstract class ExecutionNode : IEquatable /// public abstract ReadOnlySpan Conditions { get; } + /// + /// Gets the name of the source schema that this execution node operates in. + /// Returns null for nodes that are not bound to a specific source schema, + /// or whose schema is determined dynamically at runtime. + /// + public abstract string? SchemaName { get; } + /// /// Gets the execution nodes that depend on this node to be completed /// before they can be executed. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNodeType.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNodeType.cs index 8127d4004c5..85791515e73 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNodeType.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/ExecutionNodeType.cs @@ -3,6 +3,7 @@ namespace HotChocolate.Fusion.Execution.Nodes; public enum ExecutionNodeType { Operation, + OperationBatch, Introspection, Node } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/IntrospectionExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/IntrospectionExecutionNode.cs index f8014acc4d8..fa2ac4d162a 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/IntrospectionExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/IntrospectionExecutionNode.cs @@ -39,6 +39,9 @@ public IntrospectionExecutionNode( /// public override ReadOnlySpan Conditions => _conditions; + /// + public override string? SchemaName => null; + /// /// The introspection selections. /// diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/NodeFieldExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/NodeFieldExecutionNode.cs index 39dd2353a6f..88cb074adc3 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/NodeFieldExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/NodeFieldExecutionNode.cs @@ -35,6 +35,9 @@ internal NodeFieldExecutionNode( /// public override ReadOnlySpan Conditions => _conditions; + /// + public override string? SchemaName => null; + /// /// Gets the possible type branches for the node field. /// The key is the type name and the value the node to execute for that type. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs new file mode 100644 index 00000000000..c1dbbba1b4e --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationBatchExecutionNode.cs @@ -0,0 +1,268 @@ +using System.Buffers; +using System.Collections.Immutable; +using HotChocolate.Fusion.Execution.Clients; + +namespace HotChocolate.Fusion.Execution.Nodes; + +public sealed class OperationBatchExecutionNode : ExecutionNode +{ + private readonly OperationRequirement[] _requirements; + private readonly string[] _forwardedVariables; + private readonly string[] _responseNames; + private readonly ExecutionNodeCondition[] _conditions; + private readonly bool _requiresFileUpload; + private readonly OperationSourceText _operation; + private readonly int? _batchingGroupId; + private readonly string? _schemaName; + private readonly SelectionPath[] _targets; + private readonly SelectionPath _source; + + internal OperationBatchExecutionNode( + int id, + OperationSourceText operation, + string? schemaName, + SelectionPath[] targets, + SelectionPath source, + OperationRequirement[] requirements, + string[] forwardedVariables, + string[] responseNames, + ExecutionNodeCondition[] conditions, + int? batchingGroupId, + bool requiresFileUpload) + { + Id = id; + _operation = operation; + _batchingGroupId = batchingGroupId; + _schemaName = schemaName; + _targets = targets; + _source = source; + _requirements = requirements; + _forwardedVariables = forwardedVariables; + _responseNames = responseNames; + _conditions = conditions; + _requiresFileUpload = requiresFileUpload; + } + + /// + public override int Id { get; } + + /// + public override ExecutionNodeType Type => ExecutionNodeType.OperationBatch; + + /// + public override ReadOnlySpan Conditions => _conditions; + + /// + /// Gets the operation definition that this execution node represents. + /// + public OperationSourceText Operation => _operation; + + /// + /// Gets the deterministic batching group identifier assigned at planning time. + /// + public int? BatchingGroupId => _batchingGroupId; + + /// + /// Gets the response names of the target selection sets that are fulfilled by this operation. + /// + public ReadOnlySpan ResponseNames => _responseNames; + + /// + public override string? SchemaName => _schemaName; + + /// + /// Gets the paths to the selection sets for which this operation fetches data. + /// + public ReadOnlySpan Targets => _targets; + + /// + /// Gets the path to the local selection set (the selection set within the source schema request) + /// to extract the data from. + /// + public SelectionPath Source => _source; + + /// + /// Gets the data requirements that are needed to execute this operation. + /// + public ReadOnlySpan Requirements => _requirements; + + /// + /// Gets the variables that are needed to execute this operation. + /// + public ReadOnlySpan ForwardedVariables => _forwardedVariables; + + /// + /// Gets whether this operation contains one or more variables + /// that contain the Upload scalar. + /// + public bool RequiresFileUpload => _requiresFileUpload; + + protected override async ValueTask OnExecuteAsync( + OperationPlanContext context, + CancellationToken cancellationToken = default) + { + var diagnosticEvents = context.DiagnosticEvents; + var variables = context.CreateVariableValueSets(_targets, _forwardedVariables, _requirements); + + if (variables.Length == 0 && (_requirements.Length > 0 || _forwardedVariables.Length > 0)) + { + return ExecutionStatus.Skipped; + } + + var schemaName = _schemaName ?? context.GetDynamicSchemaName(this); + + context.TrackVariableValueSets(this, variables); + + var request = new SourceSchemaClientRequest + { + Node = this, + SchemaName = schemaName, + BatchingGroupId = _batchingGroupId, + OperationType = _operation.Type, + OperationSourceText = _operation.SourceText, + Variables = variables, + RequiresFileUpload = _requiresFileUpload + }; + + var index = 0; + var bufferLength = 0; + SourceSchemaResult[]? buffer = null; + var hasSomeErrors = false; + + try + { + // we execute the GraphQL request against a source schema + var response = await context.SourceSchemaScheduler + .ExecuteAsync(request, cancellationToken) + .ConfigureAwait(false); + context.TrackSourceSchemaClientResponse(this, response); + + // we read the responses from the response stream. + var totalPathCount = variables.Length; + + for (var i = 0; i < variables.Length; i++) + { + totalPathCount += variables[i].AdditionalPaths.Length; + } + + bufferLength = Math.Max(totalPathCount, 1); + buffer = ArrayPool.Shared.Rent(bufferLength); + + await foreach (var result in response.ReadAsResultStreamAsync(cancellationToken)) + { + buffer[index++] = result; + + if (result.HasErrors) + { + hasSomeErrors = true; + } + } + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // If the execution of the node was cancelled, either the entire request was cancelled + // or the execution was halted. In both cases we do not want to produce any errors + // and just exit the node as quickly as possible. + return ExecutionStatus.Failed; + } + catch (Exception exception) + { + diagnosticEvents.SourceSchemaTransportError(context, this, schemaName, exception); + + // if there is an error, we need to make sure that the pooled buffers for the JsonDocuments + // are returned to the pool. + if (buffer is not null && bufferLength > 0) + { + foreach (var result in buffer.AsSpan(0, index)) + { + // ReSharper disable once ConditionalAccessQualifierIsNonNullableAccordingToAPIContract + result?.Dispose(); + } + + buffer.AsSpan(0, index).Clear(); + ArrayPool.Shared.Return(buffer); + } + + AddErrors(context, exception, variables, _responseNames); + return ExecutionStatus.Failed; + } + + try + { + context.AddPartialResults(_source, buffer.AsSpan(0, index), _responseNames); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // If the execution of the node was cancelled, either the entire request was cancelled + // or the execution was halted. In both cases we do not want to produce any errors + // and just exit the node as quickly as possible. + return ExecutionStatus.Failed; + } + catch (Exception exception) + { + diagnosticEvents.SourceSchemaStoreError(context, this, schemaName, exception); + AddErrors(context, exception, variables, _responseNames); + return ExecutionStatus.Failed; + } + finally + { + buffer.AsSpan(0, index).Clear(); + ArrayPool.Shared.Return(buffer); + } + + return hasSomeErrors ? ExecutionStatus.PartialSuccess : ExecutionStatus.Success; + } + + protected override IDisposable CreateScope(OperationPlanContext context) + { + var schemaName = _schemaName ?? context.GetDynamicSchemaName(this); + return context.DiagnosticEvents.ExecuteOperationBatchNode(context, this, schemaName); + } + + private static void AddErrors( + OperationPlanContext context, + Exception exception, + ImmutableArray variables, + ReadOnlySpan responseNames) + { + var error = ErrorBuilder.FromException(exception).Build(); + + if (variables.Length == 0) + { + context.AddErrors(error, responseNames, Path.Root); + } + else + { + var pathBufferLength = 0; + + for (var i = 0; i < variables.Length; i++) + { + pathBufferLength += 1 + variables[i].AdditionalPaths.Length; + } + + var pathBuffer = ArrayPool.Shared.Rent(pathBufferLength); + + try + { + var pathBufferIndex = 0; + + for (var i = 0; i < variables.Length; i++) + { + pathBuffer[pathBufferIndex++] = variables[i].Path; + + foreach (var additionalPath in variables[i].AdditionalPaths) + { + pathBuffer[pathBufferIndex++] = additionalPath; + } + } + + context.AddErrors(error, responseNames, pathBuffer.AsSpan(0, pathBufferLength)); + } + finally + { + pathBuffer.AsSpan(0, pathBufferLength).Clear(); + ArrayPool.Shared.Return(pathBuffer); + } + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs index 914f86eb23d..83ee1f473bb 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/OperationExecutionNode.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Diagnostics; using System.Reactive.Disposables; +using System.Runtime.InteropServices; using HotChocolate.Fusion.Diagnostics; using HotChocolate.Fusion.Execution.Clients; @@ -15,6 +16,7 @@ public sealed class OperationExecutionNode : ExecutionNode private readonly ExecutionNodeCondition[] _conditions; private readonly bool _requiresFileUpload; private readonly OperationSourceText _operation; + private readonly int? _batchingGroupId; private readonly string? _schemaName; private readonly SelectionPath _target; private readonly SelectionPath _source; @@ -29,10 +31,12 @@ internal OperationExecutionNode( string[] forwardedVariables, string[] responseNames, ExecutionNodeCondition[] conditions, + int? batchingGroupId, bool requiresFileUpload) { Id = id; _operation = operation; + _batchingGroupId = batchingGroupId; _schemaName = schemaName; _target = target; _source = source; @@ -58,15 +62,17 @@ internal OperationExecutionNode( public OperationSourceText Operation => _operation; /// - /// Gets the response names of the selection set that are fulfilled by this operation. + /// Gets the deterministic batching group identifier assigned at planning time. /// - public ReadOnlySpan ResponseNames => _responseNames; + public int? BatchingGroupId => _batchingGroupId; /// - /// Gets the name of the source schema that this operation is executed against. - /// If null the schema is dynamic and will be set at runtime. + /// Gets the response names of the selection set that are fulfilled by this operation. /// - public string? SchemaName => _schemaName; + public ReadOnlySpan ResponseNames => _responseNames; + + /// + public override string? SchemaName => _schemaName; /// /// Gets the path to the selection set for which this operation fetches data. @@ -84,6 +90,9 @@ internal OperationExecutionNode( /// public ReadOnlySpan Requirements => _requirements; + internal ImmutableArray GetRequirementsArray() + => ImmutableCollectionsMarshal.AsImmutableArray(_requirements); + /// /// Gets the variables that are needed to execute this operation. /// @@ -113,6 +122,9 @@ protected override async ValueTask OnExecuteAsync( var request = new SourceSchemaClientRequest { + Node = this, + SchemaName = schemaName, + BatchingGroupId = _batchingGroupId, OperationType = _operation.Type, OperationSourceText = _operation.SourceText, Variables = variables, @@ -126,10 +138,10 @@ protected override async ValueTask OnExecuteAsync( try { - var client = context.GetClient(schemaName, _operation.Type); - // we execute the GraphQL request against a source schema - var response = await client.ExecuteAsync(context, this, request, cancellationToken); + var response = await context.SourceSchemaScheduler + .ExecuteAsync(request, cancellationToken) + .ConfigureAwait(false); context.TrackSourceSchemaClientResponse(this, response); // we read the responses from the response stream. @@ -226,6 +238,8 @@ internal async Task SubscribeAsync( var request = new SourceSchemaClientRequest { + Node = this, + SchemaName = schemaName, OperationType = _operation.Type, OperationSourceText = _operation.SourceText, Variables = variables @@ -237,7 +251,7 @@ internal async Task SubscribeAsync( { var client = context.GetClient(schemaName, _operation.Type); - var response = await client.ExecuteAsync(context, this, request, cancellationToken); + var response = await client.ExecuteAsync(context, request, cancellationToken); var stream = new SubscriptionEnumerable( context, diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/JsonOperationPlanFormatter.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/JsonOperationPlanFormatter.cs index c36d75d36ec..956c1d4388d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/JsonOperationPlanFormatter.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/JsonOperationPlanFormatter.cs @@ -116,6 +116,10 @@ private static void WriteNodes( WriteOperationNode(jsonWriter, operationNode, nodeTrace); break; + case OperationBatchExecutionNode batchNode: + WriteOperationBatchNode(jsonWriter, batchNode, nodeTrace); + break; + case IntrospectionExecutionNode introspectionNode: WriteIntrospectionNode(jsonWriter, introspectionNode, nodeTrace); break; @@ -170,6 +174,116 @@ private static void WriteOperationNode( jsonWriter.WriteString("target", node.Target.ToString()); } + if (node.BatchingGroupId.HasValue) + { + jsonWriter.WriteNumber("batchingGroupId", node.BatchingGroupId.Value); + } + + if (node.Requirements.Length > 0) + { + jsonWriter.WritePropertyName("requirements"); + jsonWriter.WriteStartArray(); + + foreach (var requirement in node.Requirements) + { + jsonWriter.WriteStartObject(); + jsonWriter.WriteString("name", requirement.Key); + jsonWriter.WriteString("type", requirement.Type.ToString()); + jsonWriter.WriteString("path", requirement.Path.ToString()); + jsonWriter.WriteString("selectionMap", requirement.Map.ToString()); + jsonWriter.WriteEndObject(); + } + + jsonWriter.WriteEndArray(); + } + + TryWriteConditions(jsonWriter, node); + + if (node.ForwardedVariables.Length > 0) + { + jsonWriter.WriteStartArray("forwardedVariables"); + + foreach (var variableName in node.ForwardedVariables) + { + jsonWriter.WriteStringValue(variableName); + } + + jsonWriter.WriteEndArray(); + } + + if (node.RequiresFileUpload) + { + jsonWriter.WriteBoolean("requiresFileUpload", true); + } + + if (node.Dependencies.Length > 0) + { + jsonWriter.WritePropertyName("dependencies"); + jsonWriter.WriteStartArray(); + + foreach (var dependency in node.Dependencies) + { + jsonWriter.WriteNumberValue(dependency.Id); + } + + jsonWriter.WriteEndArray(); + } + + TryWriteNodeTrace(jsonWriter, trace); + + jsonWriter.WriteEndObject(); + } + + private static void WriteOperationBatchNode( + Utf8JsonWriter jsonWriter, + OperationBatchExecutionNode node, + ExecutionNodeTrace? trace) + { + jsonWriter.WriteStartObject(); + jsonWriter.WriteNumber("id", node.Id); + jsonWriter.WriteString("type", node.Type.ToString()); + + if (!string.IsNullOrEmpty(node.SchemaName)) + { + jsonWriter.WriteString("schema", node.SchemaName); + } + + jsonWriter.WriteStartObject("operation"); + jsonWriter.WriteString("name", node.Operation.Name); + jsonWriter.WriteString("kind", node.Operation.Type.ToString()); + jsonWriter.WriteString("document", node.Operation.SourceText); + jsonWriter.WriteString("hash", node.Operation.Hash); + jsonWriter.WriteString("shortHash", node.Operation.Hash[..8]); + jsonWriter.WriteEndObject(); + + jsonWriter.WriteStartArray("responseNames"); + + foreach (var responseName in node.ResponseNames) + { + jsonWriter.WriteStringValue(responseName); + } + + jsonWriter.WriteEndArray(); + + if (!node.Source.IsRoot) + { + jsonWriter.WriteString("source", node.Source.ToString()); + } + + jsonWriter.WriteStartArray("targets"); + + foreach (var target in node.Targets) + { + jsonWriter.WriteStringValue(target.ToString()); + } + + jsonWriter.WriteEndArray(); + + if (node.BatchingGroupId.HasValue) + { + jsonWriter.WriteNumber("batchingGroupId", node.BatchingGroupId.Value); + } + if (node.Requirements.Length > 0) { jsonWriter.WritePropertyName("requirements"); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/JsonOperationPlanParser.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/JsonOperationPlanParser.cs index 5b35aff05a9..c7a2fd2e662 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/JsonOperationPlanParser.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/JsonOperationPlanParser.cs @@ -78,6 +78,7 @@ private ImmutableArray ParseNodes(JsonElement nodesElement, Opera (ExecutionNode, int[]?, Dictionary?, int?) node = nodeType switch { "Operation" => ParseOperationNode(nodeElement, id), + "OperationBatch" => ParseOperationBatchNode(nodeElement, id), "Introspection" => ParseIntrospectionNode(nodeElement, id, operation), "Node" => ParseNodeFieldNode(nodeElement, id, operation), _ => throw new NotSupportedException($"Unsupported node type: {nodeType}") @@ -169,6 +170,7 @@ private static (OperationExecutionNode, int[]?, Dictionary?, int?) string[]? forwardedVariables = null; string[]? responseNames = null; int[]? dependencies = null; + int? batchingGroupId = null; if (nodeElement.TryGetProperty("source", out var sourceElement)) { @@ -223,6 +225,11 @@ private static (OperationExecutionNode, int[]?, Dictionary?, int?) .ToArray(); } + if (nodeElement.TryGetProperty("batchingGroupId", out var batchingGroupIdElement)) + { + batchingGroupId = batchingGroupIdElement.GetInt32(); + } + var conditions = TryParseConditions(nodeElement); var requiresFileUpload = nodeElement.TryGetProperty("requiresFileUpload", out var requiresFileUploadElement) @@ -242,6 +249,111 @@ private static (OperationExecutionNode, int[]?, Dictionary?, int?) forwardedVariables ?? [], responseNames ?? [], conditions, + batchingGroupId, + requiresFileUpload); + + return (node, dependencies, null, null); + } + + private static (OperationBatchExecutionNode, int[]?, Dictionary?, int?) ParseOperationBatchNode( + JsonElement nodeElement, int id) + { + string? schemaName = null; + if (nodeElement.TryGetProperty("schema", out var schemaElement)) + { + schemaName = schemaElement.GetString()!; + } + + var operationElement = nodeElement.GetProperty("operation"); + var operationName = operationElement.GetProperty("name").GetString()!; + var operationType = Enum.Parse(operationElement.GetProperty("kind").GetString()!); + var document = operationElement.GetProperty("document").GetString()!; + var hash = operationElement.GetProperty("hash").GetString()!; + + SelectionPath? source = null; + List? requirements = null; + string[]? forwardedVariables = null; + string[]? responseNames = null; + int[]? dependencies = null; + int? batchingGroupId = null; + + if (nodeElement.TryGetProperty("source", out var sourceElement)) + { + source = SelectionPath.Parse(sourceElement.GetString()!); + } + + var targets = nodeElement.TryGetProperty("targets", out var targetsElement) + ? targetsElement.EnumerateArray().Select(e => SelectionPath.Parse(e.GetString()!)).ToArray() + : []; + + if (nodeElement.TryGetProperty("requirements", out var requirementsElement)) + { + requirements = []; + + foreach (var requirementElement in requirementsElement.EnumerateArray()) + { + var requirementName = requirementElement.GetProperty("name").GetString()!; + var requirementType = requirementElement.GetProperty("type").GetString()!; + var requirementPath = requirementElement.GetProperty("path").GetString()!; + var selectionMap = requirementElement.GetProperty("selectionMap").GetString()!; + + requirements.Add(new OperationRequirement( + requirementName, + Utf8GraphQLParser.Syntax.ParseTypeReference(requirementType), + SelectionPath.Parse(requirementPath), + FieldSelectionMapParser.Parse(selectionMap))); + } + } + + if (nodeElement.TryGetProperty("forwardedVariables", out var forwardedVariablesElement)) + { + forwardedVariables = forwardedVariablesElement + .EnumerateArray() + .Select(e => e.GetString()!) + .ToArray(); + } + + if (nodeElement.TryGetProperty("responseNames", out var responseNamesElement)) + { + responseNames = responseNamesElement + .EnumerateArray() + .Select(e => e.GetString()!) + .ToArray(); + } + + if (nodeElement.TryGetProperty("dependencies", out var dependenciesElement)) + { + dependencies = dependenciesElement + .EnumerateArray() + .Select(e => e.GetInt32()) + .ToArray(); + } + + if (nodeElement.TryGetProperty("batchingGroupId", out var batchingGroupIdElement)) + { + batchingGroupId = batchingGroupIdElement.GetInt32(); + } + + var conditions = TryParseConditions(nodeElement); + + var requiresFileUpload = nodeElement.TryGetProperty("requiresFileUpload", out var requiresFileUploadElement) + && requiresFileUploadElement.ValueKind == JsonValueKind.True; + + var node = new OperationBatchExecutionNode( + id, + new OperationSourceText( + operationName, + operationType, + document, + hash), + schemaName, + targets, + source ?? SelectionPath.Root, + requirements?.ToArray() ?? [], + forwardedVariables ?? [], + responseNames ?? [], + conditions, + batchingGroupId, requiresFileUpload); return (node, dependencies, null, null); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/YamlOperationPlanFormatter.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/YamlOperationPlanFormatter.cs index 730cc5d0a6b..e8499be2cc9 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/YamlOperationPlanFormatter.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Nodes/Serialization/YamlOperationPlanFormatter.cs @@ -25,6 +25,10 @@ public override string Format(OperationPlan plan, OperationPlanTrace? trace = nu WriteOperationNode(operationNode, nodeTrace, writer); break; + case OperationBatchExecutionNode batchNode: + WriteOperationBatchNode(batchNode, nodeTrace, writer); + break; + case IntrospectionExecutionNode introspectionNode: WriteIntrospectionNode(introspectionNode, nodeTrace, writer); break; @@ -124,6 +128,117 @@ private static void WriteOperationNode(OperationExecutionNode node, ExecutionNod writer.WriteLine("target: {0}", node.Target.ToString()); } + if (node.BatchingGroupId.HasValue) + { + writer.WriteLine("batchingGroupId: {0}", node.BatchingGroupId.Value); + } + + if (node.Requirements.Length > 0) + { + writer.WriteLine("requirements:"); + writer.Indent(); + foreach (var requirement in node.Requirements.ToArray().OrderBy(t => t.Key)) + { + writer.WriteLine("- name: {0}", requirement.Key); + writer.Indent(); + + writer.WriteLine("selectionMap: >-"); + writer.Indent(); + var selectionMapReader = new StringReader(requirement.Map.ToString(indented: true)); + var selectionMapLine = selectionMapReader.ReadLine(); + while (selectionMapLine != null) + { + writer.WriteLine(selectionMapLine); + selectionMapLine = selectionMapReader.ReadLine(); + } + writer.Unindent(); + writer.Unindent(); + } + + writer.Unindent(); + } + + TryWriteConditions(writer, node); + + if (node.ForwardedVariables.Length > 0) + { + writer.WriteLine("forwardedVariables:"); + writer.Indent(); + + foreach (var variableName in node.ForwardedVariables) + { + writer.WriteLine("- {0}", variableName); + } + + writer.Unindent(); + } + + if (node.RequiresFileUpload) + { + writer.WriteLine("requiresFileUpload: true"); + } + + if (node.Dependencies.Length > 0) + { + writer.WriteLine("dependencies:"); + writer.Indent(); + foreach (var dependency in node.Dependencies.ToArray().OrderBy(t => t.Id)) + { + writer.WriteLine("- id: {0}", dependency.Id); + } + + writer.Unindent(); + } + + TryWriteNodeTrace(writer, trace); + + writer.Unindent(); + } + + private static void WriteOperationBatchNode(OperationBatchExecutionNode node, ExecutionNodeTrace? trace, CodeWriter writer) + { + writer.WriteLine("- id: {0}", node.Id); + writer.Indent(); + + writer.WriteLine("type: {0}", "OperationBatch"); + + if (node.SchemaName is not null) + { + writer.WriteLine("schema: {0}", node.SchemaName); + } + + writer.WriteLine("operation: |"); + writer.Indent(); + var reader = new StringReader(node.Operation.SourceText); + var line = reader.ReadLine(); + while (line != null) + { + writer.WriteLine(line); + line = reader.ReadLine(); + } + writer.Unindent(); + + if (!node.Source.IsRoot) + { + writer.WriteLine("source: {0}", node.Source.ToString()); + } + + if (node.Targets.Length > 0) + { + writer.WriteLine("targets:"); + writer.Indent(); + foreach (var target in node.Targets) + { + writer.WriteLine("- {0}", target.ToString()); + } + writer.Unindent(); + } + + if (node.BatchingGroupId.HasValue) + { + writer.WriteLine("batchingGroupId: {0}", node.BatchingGroupId.Value); + } + if (node.Requirements.Length > 0) { writer.WriteLine("requirements:"); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs index 017bf11c082..7d74b5a67c7 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanContext.cs @@ -1,4 +1,4 @@ -using System.Collections.Concurrent; +using System.Buffers; using System.Collections.Immutable; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -20,11 +20,16 @@ namespace HotChocolate.Fusion.Execution; public sealed class OperationPlanContext : IFeatureProvider, IAsyncDisposable { private static readonly JsonOperationPlanFormatter s_planFormatter = new(); - private readonly ConcurrentDictionary> _nodesToComplete = new(); - private readonly ConcurrentDictionary _nodeContexts = new(); + private readonly NodeCompletionSet?[] _nodesToComplete; + private readonly int _dependentBitsetWordCount; + private readonly string?[] _schemaNames; + private readonly ImmutableArray[] _variableValueSets; + private readonly Uri?[] _transportUris; + private readonly string?[] _transportContentTypes; private readonly IFusionExecutionDiagnosticEvents _diagnosticEvents; private readonly FetchResultStore _resultStore; private readonly ExecutionState _executionState; + private readonly SourceSchemaRequestDispatcher _sourceSchemaDispatcher; private readonly INodeIdParser _nodeIdParser; private readonly bool _collectTelemetry; private ISourceSchemaClientScope _clientScope; @@ -69,6 +74,25 @@ public OperationPlanContext( IncludeFlags); _executionState = new ExecutionState(_collectTelemetry, cancellationTokenSource); + _sourceSchemaDispatcher = new SourceSchemaRequestDispatcher(this); + + var maxNodeId = 0; + + foreach (var executionNode in operationPlan.AllNodes) + { + if (executionNode.Id > maxNodeId) + { + maxNodeId = executionNode.Id; + } + } + + var nodeSlotCount = maxNodeId + 1; + _dependentBitsetWordCount = (maxNodeId >> 6) + 1; + _nodesToComplete = new NodeCompletionSet?[nodeSlotCount]; + _schemaNames = new string?[nodeSlotCount]; + _variableValueSets = new ImmutableArray[nodeSlotCount]; + _transportUris = new Uri?[nodeSlotCount]; + _transportContentTypes = new string?[nodeSlotCount]; } public OperationPlan OperationPlan { get; } @@ -81,6 +105,10 @@ public OperationPlanContext( public ISourceSchemaClientScope ClientScope => _clientScope; + public ISourceSchemaScheduler SourceSchemaScheduler => _sourceSchemaDispatcher; + + public ISourceSchemaDispatcher SourceSchemaDispatcher => _sourceSchemaDispatcher; + internal ExecutionState ExecutionState => _executionState; public ulong IncludeFlags { get; } @@ -100,33 +128,34 @@ public OperationPlanContext( internal void EnqueueForExecution(ExecutionNode node, ExecutionNode dependentNode) { - var dependentNodes = _nodesToComplete.GetOrAdd(node.Id, _ => [dependentNode]); - if (!dependentNodes.Contains(dependentNode)) + var nodeId = node.Id; + var nodeCompletionSet = _nodesToComplete[nodeId]; + + if (nodeCompletionSet is null) { - dependentNodes.Add(dependentNode); + var newSet = new NodeCompletionSet(_dependentBitsetWordCount); + nodeCompletionSet = Interlocked.CompareExchange(ref _nodesToComplete[nodeId], newSet, null) ?? newSet; } + + nodeCompletionSet.Add(dependentNode); } internal ImmutableArray GetDependentsToExecute(ExecutionNode node) - => _nodesToComplete.TryGetValue(node.Id, out var nodesToComplete) - ? [.. nodesToComplete] - : []; - - internal void SetDynamicSchemaName(ExecutionNode node, string schemaName) { - _nodeContexts.AddOrUpdate( - node.Id, - static (_, schemaName) => new NodeContext { SchemaName = schemaName }, - static (_, context, schemaName) => context with { SchemaName = schemaName }, - schemaName); + var nodeCompletionSet = _nodesToComplete[node.Id]; + return nodeCompletionSet?.GetSnapshot() ?? []; } + internal void SetDynamicSchemaName(ExecutionNode node, string schemaName) + => _schemaNames[node.Id] = schemaName; + public string GetDynamicSchemaName(ExecutionNode node) { - if (_nodeContexts.TryGetValue(node.Id, out var context) - && !string.IsNullOrEmpty(context.SchemaName)) + var schemaName = _schemaNames[node.Id]; + + if (!string.IsNullOrEmpty(schemaName)) { - return context.SchemaName; + return schemaName; } throw new InvalidOperationException( @@ -144,11 +173,7 @@ internal void TrackVariableValueSets(ExecutionNode node, ImmutableArray new NodeContext { Variables = variableValueSets }, - static (_, context, variableValueSets) => context with { Variables = variableValueSets }, - variableValueSets); + _variableValueSets[node.Id] = variableValueSets; } internal ImmutableArray GetVariableValueSets(ExecutionNode node) @@ -158,9 +183,8 @@ internal ImmutableArray GetVariableValueSets(ExecutionNode node) return []; } - return _nodeContexts.TryGetValue(node.Id, out var context) - ? context.Variables - : []; + var variableValueSets = _variableValueSets[node.Id]; + return variableValueSets.IsDefault ? [] : variableValueSets; } internal void TrackSourceSchemaClientResponse(ExecutionNode node, SourceSchemaClientResponse result) @@ -170,11 +194,8 @@ internal void TrackSourceSchemaClientResponse(ExecutionNode node, SourceSchemaCl return; } - _nodeContexts.AddOrUpdate( - node.Id, - static (_, result) => new NodeContext { Uri = result.Uri, ContentType = result.ContentType }, - static (_, context, result) => context with { Uri = result.Uri, ContentType = result.ContentType }, - result); + _transportUris[node.Id] = result.Uri; + _transportContentTypes[node.Id] = result.ContentType; } internal (Uri? Uri, string? ContentType) GetTransportDetails(ExecutionNode node) @@ -184,9 +205,7 @@ internal void TrackSourceSchemaClientResponse(ExecutionNode node, SourceSchemaCl return (null, null); } - return _nodeContexts.TryGetValue(node.Id, out var context) - ? (context.Uri, context.ContentType) - : (null, null); + return (_transportUris[node.Id], _transportContentTypes[node.Id]); } internal void CompleteNode(ExecutionNodeResult result) @@ -216,6 +235,28 @@ internal ImmutableArray CreateVariableValueSets( } } + internal ImmutableArray CreateVariableValueSets( + ReadOnlySpan selectionSets, + ReadOnlySpan forwardedVariables, + ReadOnlySpan requiredData) + { + if (requiredData.Length == 0) + { + if (forwardedVariables.Length == 0) + { + return []; + } + + var variableValues = GetPathThroughVariables(forwardedVariables); + return [new VariableValues(Path.Root, new ObjectValueNode(variableValues))]; + } + else + { + var variableValues = GetPathThroughVariables(forwardedVariables); + return _resultStore.CreateVariableValueSets(selectionSets, variableValues, requiredData); + } + } + internal void AddPartialResults( SelectionPath sourcePath, ReadOnlySpan results, @@ -254,6 +295,8 @@ internal PooledArrayWriter CreateRentedBuffer() internal void Begin(long? start = null, string? traceId = null) { + ResetNodeState(); + if (_collectTelemetry) { _start = start ?? Stopwatch.GetTimestamp(); @@ -375,19 +418,121 @@ public async ValueTask DisposeAsync() if (!_disposed) { _disposed = true; + DisposeNodeState(); + _sourceSchemaDispatcher.Abort(); _resultStore.Dispose(); await _clientScope.DisposeAsync(); } } - private sealed record NodeContext + private void ResetNodeState() { - public string? SchemaName { get; init; } + Array.Clear(_schemaNames); - public ImmutableArray Variables { get; init; } = []; + if (_collectTelemetry) + { + Array.Clear(_variableValueSets); + Array.Clear(_transportUris); + Array.Clear(_transportContentTypes); + } - public Uri? Uri { get; init; } + foreach (var nodeCompletionSet in _nodesToComplete) + { + nodeCompletionSet?.Reset(); + } + } + + private void DisposeNodeState() + { + foreach (var nodeCompletionSet in _nodesToComplete) + { + nodeCompletionSet?.Dispose(); + } + } + + private sealed class NodeCompletionSet(int bitsetWordCount) : IDisposable + { + private readonly object _sync = new(); + private ExecutionNode[] _dependents = []; + private ulong[]? _seenDependents; + private int _count; + + public void Add(ExecutionNode node) + { + lock (_sync) + { + _seenDependents ??= RentBitset(bitsetWordCount); + + var nodeId = node.Id; + var index = nodeId >> 6; + var bit = 1UL << (nodeId & 63); + + if ((_seenDependents[index] & bit) != 0) + { + return; + } + + _seenDependents[index] |= bit; + + if (_dependents.Length == 0) + { + _dependents = new ExecutionNode[2]; + } + else if (_count == _dependents.Length) + { + Array.Resize(ref _dependents, _dependents.Length * 2); + } + + _dependents[_count++] = node; + } + } - public string? ContentType { get; init; } + public ImmutableArray GetSnapshot() + { + lock (_sync) + { + if (_count == 0) + { + return []; + } + + return ImmutableArray.Create(_dependents, 0, _count); + } + } + + public void Reset() + { + lock (_sync) + { + _count = 0; + + if (_seenDependents is not null) + { + Array.Clear(_seenDependents, 0, bitsetWordCount); + } + } + } + + public void Dispose() + { + lock (_sync) + { + if (_seenDependents is not null) + { + ArrayPool.Shared.Return(_seenDependents, clearArray: true); + _seenDependents = null; + } + + _count = 0; + _dependents = []; + } + } + + private static ulong[] RentBitset(int bitsetWordCount) + { + var seenDependents = ArrayPool.Shared.Rent(bitsetWordCount); + Array.Clear(seenDependents, 0, bitsetWordCount); + return seenDependents; + } } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanExecutor.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanExecutor.cs index abe4de8c703..985e9281d61 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanExecutor.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/OperationPlanExecutor.cs @@ -21,26 +21,39 @@ public async Task ExecuteAsync( await using var context = new OperationPlanContext(requestContext, variables, operationPlan, executionCts); context.Begin(); - switch (operationPlan.Operation.Definition.Operation) + try { - case OperationType.Query: - await ExecuteQueryAsync(context, operationPlan, executionCts.Token); - break; + switch (operationPlan.Operation.Definition.Operation) + { + case OperationType.Query: + await ExecuteQueryAsync(context, operationPlan, executionCts.Token); + break; - case OperationType.Mutation: - await ExecuteMutationAsync(context, operationPlan, executionCts.Token); - break; + case OperationType.Mutation: + await ExecuteMutationAsync(context, operationPlan, executionCts.Token); + break; - default: - throw new InvalidOperationException("Only queries and mutations can be executed."); - } + default: + throw new InvalidOperationException("Only queries and mutations can be executed."); + } + + if (executionCts.IsCancellationRequested) + { + context.SourceSchemaDispatcher.Abort(); + } - // If the original CancellationToken of the request was cancelled, - // the Execution nodes and the PlanExecutor should have been gracefully cancelled, - // so we throw here to properly cancel the request execution. - cancellationToken.ThrowIfCancellationRequested(); + // If the original CancellationToken of the request was cancelled, + // the Execution nodes and the PlanExecutor should have been gracefully cancelled, + // so we throw here to properly cancel the request execution. + cancellationToken.ThrowIfCancellationRequested(); - return context.Complete(); + return context.Complete(); + } + catch (Exception ex) + { + context.SourceSchemaDispatcher.Abort(ex); + throw; + } } public async Task SubscribeAsync( @@ -62,14 +75,20 @@ public async Task SubscribeAsync( // We create a new CancellationTokenSource that can be used to halt the execution engine, // without also cancelling the entire request pipeline. var executionCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + OperationPlanContext? context = null; try { - var context = new OperationPlanContext(requestContext, operationPlan, executionCts); + context = new OperationPlanContext(requestContext, operationPlan, executionCts); var subscriptionResult = await subscriptionNode.SubscribeAsync(context, executionCts.Token); var executionState = context.ExecutionState; - executionCts.Token.Register(() => executionState.Signal.TryResetToIdle()); + executionCts.Token.Register( + () => + { + executionState.Signal.TryResetToIdle(); + context.SourceSchemaDispatcher.Abort(); + }); if (subscriptionResult.Status is not ExecutionStatus.Success) { @@ -88,8 +107,9 @@ public async Task SubscribeAsync( stream.RegisterForCleanup(executionCts); return stream; } - catch + catch (Exception ex) { + context?.SourceSchemaDispatcher.Abort(ex); executionCts.Dispose(); throw; @@ -103,7 +123,14 @@ private static async Task ExecuteQueryAsync( { var executionState = context.ExecutionState; - cancellationToken.Register(() => executionState.Signal.TryResetToIdle()); + await using var cancellationRegistration = cancellationToken.Register( + () => + { + executionState.Signal.TryResetToIdle(); + context.SourceSchemaDispatcher.Abort(); + }); + + RegisterBatchingGroups(context, plan); // GraphQL queries allow us to execute the plan by using full parallelism. // We fill the backlog with all nodes from the operation plan. @@ -120,7 +147,7 @@ private static async Task ExecuteQueryAsync( while (executionState.TryDequeueCompletedResult(out var result)) { var node = plan.GetNodeById(result.Id); - executionState.CompleteNode(node, result); + executionState.CompleteNode(context, node, result); } executionState.EnqueueNextNodes(context, cancellationToken); @@ -148,7 +175,14 @@ private static async Task ExecuteMutationAsync( { var executionState = context.ExecutionState; - cancellationToken.Register(() => executionState.Signal.TryResetToIdle()); + await using var cancellationRegistration = cancellationToken.Register( + () => + { + executionState.Signal.TryResetToIdle(); + context.SourceSchemaDispatcher.Abort(); + }); + + RegisterBatchingGroups(context, plan); // For mutations, we fill the backlog with all nodes from the operation plan just like for queries. executionState.FillBacklog(plan); @@ -170,7 +204,7 @@ private static async Task ExecuteMutationAsync( while (executionState.TryDequeueCompletedResult(out var result)) { var node = plan.GetNodeById(result.Id); - executionState.CompleteNode(node, result); + executionState.CompleteNode(context, node, result); } executionState.EnqueueNextNodes(context, cancellationToken); @@ -208,6 +242,12 @@ private static async IAsyncEnumerable CreateSubscriptionEnumera var executionState = context.ExecutionState; var stream = subscriptionResult.ReadStreamAsync() .WithCancellation(executionCancellationToken); + await using var cancellationRegistration = executionCancellationToken.Register( + () => + { + executionState.Signal.TryResetToIdle(); + context.SourceSchemaDispatcher.Abort(); + }); await foreach (var eventArgs in stream) { @@ -224,6 +264,9 @@ private static async IAsyncEnumerable CreateSubscriptionEnumera context.Begin(eventArgs.StartTimestamp, eventArgs.Activity?.TraceId.ToHexString()); executionState.Reset(); + context.SourceSchemaDispatcher.Reset(); + + RegisterBatchingGroups(context, plan); executionState.FillBacklog(plan); executionState.EnqueueForCompletion( new ExecutionNodeResult( @@ -240,7 +283,7 @@ private static async IAsyncEnumerable CreateSubscriptionEnumera while (executionState.TryDequeueCompletedResult(out var nodeResult)) { var node = plan.GetNodeById(nodeResult.Id); - executionState.CompleteNode(node, nodeResult); + executionState.CompleteNode(context, node, nodeResult); } executionState.EnqueueNextNodes(context, executionCancellationToken); @@ -264,6 +307,7 @@ private static async IAsyncEnumerable CreateSubscriptionEnumera } catch (Exception ex) when (ex is not OperationCanceledException) { + context.SourceSchemaDispatcher.Abort(ex); context.DiagnosticEvents.SubscriptionEventError( context, subscriptionNode, @@ -281,4 +325,45 @@ private static async IAsyncEnumerable CreateSubscriptionEnumera yield return result; } } + + private static void RegisterBatchingGroups(OperationPlanContext context, OperationPlan plan) + { + Dictionary>? groups = null; + + foreach (var executionNode in plan.AllNodes) + { + var groupId = executionNode switch + { + OperationExecutionNode n => n.BatchingGroupId, + OperationBatchExecutionNode n => n.BatchingGroupId, + _ => null + }; + + if (groupId is null) + { + continue; + } + + groups ??= []; + + if (!groups.TryGetValue(groupId.Value, out var nodeIds)) + { + nodeIds = []; + groups.Add(groupId.Value, nodeIds); + } + + nodeIds.Add(executionNode.Id); + } + + if (groups is null) + { + return; + } + + foreach (var (groupId, nodeIds) in groups) + { + nodeIds.TrimExcess(); + context.SourceSchemaDispatcher.RegisterGroup(groupId, nodeIds); + } + } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs index 0b7cc9db9d2..00caae77665 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Execution/Results/FetchResultStore.cs @@ -18,7 +18,11 @@ namespace HotChocolate.Fusion.Execution.Results; internal sealed class FetchResultStore : IDisposable { - private readonly ReaderWriterLockSlim _lock = new(LockRecursionPolicy.NoRecursion); +#if NET9_0_OR_GREATER + private readonly Lock _lock = new(); +#else + private readonly object _lock = new(); +#endif private readonly ISchemaDefinition _schema; private readonly IErrorHandler _errorHandler; private readonly Operation _operation; @@ -117,8 +121,11 @@ public bool AddPartialResults( if (errors?.RootErrors is { Length: > 0 } rootErrors) { - _errors ??= []; - _errors.AddRange(rootErrors); + lock (_lock) + { + _errors ??= []; + _errors.AddRange(rootErrors); + } } dataElement = GetDataElement(sourcePath, result.Data); @@ -158,9 +165,7 @@ public bool AddPartialResults(SourceResultDocument document, ReadOnlySpan responseNames, params ReadOnlySpan paths) { - _lock.EnterWriteLock(); - - try + lock (_lock) { ref var path = ref MemoryMarshal.GetReference(paths); ref var end = ref Unsafe.Add(ref path, paths.Length); @@ -220,10 +219,6 @@ public bool AddErrors(IError error, ReadOnlySpan responseNames, params R path = ref Unsafe.Add(ref path, 1)!; } } - finally - { - _lock.ExitWriteLock(); - } return true; } @@ -234,9 +229,7 @@ private bool SaveSafe( ReadOnlySpan errorTries, ReadOnlySpan responseNames) { - _lock.EnterWriteLock(); - - try + lock (_lock) { ref var result = ref MemoryMarshal.GetReference(results); ref var data = ref MemoryMarshal.GetReference(dataElements); @@ -276,10 +269,6 @@ private bool SaveSafe( errorTrie = ref Unsafe.Add(ref errorTrie, 1)!; } } - finally - { - _lock.ExitWriteLock(); - } return true; } @@ -300,141 +289,196 @@ public ImmutableArray CreateVariableValueSets( nameof(requiredData)); } - _lock.EnterReadLock(); - - try + lock (_lock) { - var current = new List { _result.Data }; - var next = new List(); + var elements = CollectTargetElements(selectionSet); - for (var i = 0; i < selectionSet.Segments.Length; i++) + if (elements is null) { - var segment = selectionSet.Segments[i]; + return []; + } - foreach (var element in current) - { - if (segment.Kind is SelectionPathSegmentKind.InlineFragment) - { - if (element.TryGetProperty(IntrospectionFieldNames.TypeNameSpan, out var value) - && value.ValueKind is JsonValueKind.String - && value.TextEqualsHelper(segment.Name, isPropertyName: false)) - { - next.Add(element); - } - } - else if (segment.Kind is SelectionPathSegmentKind.Field) - { - if (!element.TryGetProperty(segment.Name, out var value)) - { - continue; - } - - var valueKind = value.ValueKind; - - if (valueKind is JsonValueKind.Null or JsonValueKind.Undefined) - { - continue; - } - - if (valueKind is JsonValueKind.Array) - { - next.AddRange(UnrollLists(value)); - continue; - } - - if (valueKind is JsonValueKind.Object) - { - next.Add(value); - continue; - } - - // TODO : Better error - throw new NotSupportedException("Must be list or object."); - } - } + return BuildVariableValueSets(elements, requestVariables, requiredData); + } + } - (next, current) = (current, next); - next.Clear(); + /// + /// Creates a deduplicated set of variable values across multiple target selection paths. + /// Elements from all targets are combined and deduplication is applied globally, so that + /// entities at different target locations that produce identical variable values are merged + /// into a single entry via . + /// + public ImmutableArray CreateVariableValueSets( + ReadOnlySpan selectionSets, + IReadOnlyList requestVariables, + ReadOnlySpan requiredData) + { + ObjectDisposedException.ThrowIf(_disposed, this); + ArgumentNullException.ThrowIfNull(requestVariables); + + if (requiredData.Length == 0) + { + throw new ArgumentException( + "The required data span must contain at least one requirement.", + nameof(requiredData)); + } + + lock (_lock) + { + var combined = new List(); - if (current.Count == 0) + foreach (var selectionSet in selectionSets) + { + var elements = CollectTargetElements(selectionSet); + + if (elements is not null) { - return []; + combined.AddRange(elements); } } - PooledArrayWriter? buffer = null; - VariableValues[]? variableValueSets = null; - Dictionary? seen = null; - List?[]? additionalPaths = null; - var nextIndex = 0; - - foreach (var result in current) + if (combined.Count == 0) { - var variables = MapRequirements( - result, - requestVariables, - requiredData, - ref buffer); + return []; + } + + return BuildVariableValueSets(combined, requestVariables, requiredData); + } + } + + // Caller must hold _lock for reading. + private List? CollectTargetElements(SelectionPath selectionSet) + { + var current = new List { _result.Data }; + var next = new List(); + + for (var i = 0; i < selectionSet.Segments.Length; i++) + { + var segment = selectionSet.Segments[i]; - if (variables is null) + foreach (var element in current) + { + if (segment.Kind is SelectionPathSegmentKind.InlineFragment) { - continue; + if (element.TryGetProperty(IntrospectionFieldNames.TypeNameSpan, out var value) + && value.ValueKind is JsonValueKind.String + && value.TextEqualsHelper(segment.Name, isPropertyName: false)) + { + next.Add(element); + } } + else if (segment.Kind is SelectionPathSegmentKind.Field) + { + if (!element.TryGetProperty(segment.Name, out var value)) + { + continue; + } - variableValueSets ??= new VariableValues[current.Count]; + var valueKind = value.ValueKind; - if (nextIndex > 0) - { - seen ??= new Dictionary(VariableValueComparer.Instance) + if (valueKind is JsonValueKind.Null or JsonValueKind.Undefined) { - [variableValueSets[0].Values] = 0 - }; + continue; + } + + if (valueKind is JsonValueKind.Array) + { + next.AddRange(UnrollLists(value)); + continue; + } - if (seen.TryGetValue(variables, out var existingIndex)) + if (valueKind is JsonValueKind.Object) { - additionalPaths ??= new List?[current.Count]; - (additionalPaths[existingIndex] ??= []).Add(result.Path); + next.Add(value); continue; } - seen[variables] = nextIndex; + // TODO : Better error + throw new NotSupportedException("Must be list or object."); } + } + + (next, current) = (current, next); + next.Clear(); - variableValueSets[nextIndex++] = new VariableValues(result.Path, variables); + if (current.Count == 0) + { + return null; } + } + + return current; + } + + private ImmutableArray BuildVariableValueSets( + List elements, + IReadOnlyList requestVariables, + ReadOnlySpan requiredData) + { + PooledArrayWriter? buffer = null; + VariableValues[]? variableValueSets = null; + Dictionary? seen = null; + List?[]? additionalPaths = null; + var nextIndex = 0; - if (additionalPaths is not null) + foreach (var result in elements) + { + var variables = MapRequirements(result, requestVariables, requiredData, ref buffer); + + if (variables is null) { - for (var i = 0; i < nextIndex; i++) - { - if (additionalPaths[i] is { } paths) - { - variableValueSets![i] = variableValueSets[i] with - { - AdditionalPaths = [.. paths] - }; - } - } + continue; } - if (variableValueSets?.Length > 0) + variableValueSets ??= new VariableValues[elements.Count]; + + if (nextIndex > 0) { - Array.Resize(ref variableValueSets, nextIndex); + seen ??= new Dictionary(VariableValueComparer.Instance) + { + [variableValueSets[0].Values] = 0 + }; + + if (seen.TryGetValue(variables, out var existingIndex)) + { + additionalPaths ??= new List?[elements.Count]; + (additionalPaths[existingIndex] ??= []).Add(result.Path); + continue; + } + + seen[variables] = nextIndex; } - if (buffer is not null) + variableValueSets[nextIndex++] = new VariableValues(result.Path, variables); + } + + if (additionalPaths is not null) + { + for (var i = 0; i < nextIndex; i++) { - _memory.Push(buffer); + if (additionalPaths[i] is { } paths) + { + variableValueSets![i] = variableValueSets[i] with + { + AdditionalPaths = [.. paths] + }; + } } + } - return variableValueSets is not null - ? ImmutableCollectionsMarshal.AsImmutableArray(variableValueSets) - : []; + if (variableValueSets?.Length > 0) + { + Array.Resize(ref variableValueSets, nextIndex); } - finally + + if (buffer is not null) { - _lock.ExitReadLock(); + _memory.Push(buffer); } + + return variableValueSets is not null + ? ImmutableCollectionsMarshal.AsImmutableArray(variableValueSets) + : []; } private ObjectValueNode? MapRequirements( @@ -636,8 +680,6 @@ public void Dispose() _disposed = true; - _lock.Dispose(); - while (_memory.TryPop(out var memory)) { memory.Dispose(); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs index 1f2f64de61d..e0ebd290913 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlanner.BuildExecutionTree.cs @@ -11,6 +11,7 @@ namespace HotChocolate.Fusion.Planning; public sealed partial class OperationPlanner { private const string UploadScalarName = "Upload"; + private const string DynamicSchemaNameMarker = "__dynamic__"; /// /// Builds the actual execution plan from the provided . @@ -49,11 +50,13 @@ private OperationPlan BuildExecutionPlan( completedNodes, dependencyLookup, _schema, + _options.EnableRequestGrouping, hasVariables); + MergeEquivalentOperationNodes(completedNodes, dependencyLookup); BuildDependencyStructure(completedNodes, dependencyLookup, branchesLookup, fallbackLookup); var rootNodes = planSteps - .Where(t => !dependencyLookup.ContainsKey(t.Id)) + .Where(t => !dependencyLookup.ContainsKey(t.Id) && completedNodes.ContainsKey(t.Id)) .Select(t => completedNodes[t.Id]) .ToImmutableArray(); @@ -229,10 +232,15 @@ private static void BuildExecutionNodes( Dictionary completedNodes, Dictionary> dependencyLookup, ISchemaDefinition schema, + bool enableRequestGrouping, bool hasVariables) { var hasUploadScalar = schema.Types.TryGetType(UploadScalarName, out var uploadType) && uploadType.IsScalarType(); + var batchingGroupLookup = CreateBatchingGroupLookup( + planSteps, + dependencyLookup, + enableRequestGrouping); var readySteps = planSteps.Where(t => !dependencyLookup.ContainsKey(t.Id)).ToList(); List? variables = null; @@ -281,9 +289,13 @@ private static void BuildExecutionNodes( var requiresFileUpload = hasUploadScalar && DoVariablesContainUploadScalar(operationStep.Definition.VariableDefinitions, schema); + var operation = RemoveEmptyTypeNames(operationStep.Definition); + var operationSource = operation.ToSourceText(); + int? batchingGroupId = batchingGroupLookup.TryGetValue(step.Id, out var groupId) ? groupId : null; + var node = new OperationExecutionNode( operationStep.Id, - RemoveEmptyTypeNames(operationStep.Definition).ToSourceText(), + operationSource, operationStep.SchemaName, operationStep.Target, operationStep.Source, @@ -291,6 +303,7 @@ private static void BuildExecutionNodes( variables?.Count > 0 ? variables.ToArray() : [], GetResponseNamesFromPath(operationStep.Definition, operationStep.Source), operationStep.Conditions, + batchingGroupId, requiresFileUpload); completedNodes.Add(step.Id, node); @@ -325,6 +338,138 @@ private static void BuildExecutionNodes( } } + internal static Dictionary CreateBatchingGroupLookup( + ImmutableList planSteps, + Dictionary> dependencyLookup, + bool enableRequestGrouping) + { + if (!enableRequestGrouping) + { + return []; + } + + var queryStepsByService = new Dictionary>(StringComparer.Ordinal); + + foreach (var operationStep in planSteps.OfType()) + { + if (operationStep.Definition.Operation is not OperationType.Query) + { + continue; + } + + var schemaKey = operationStep.SchemaName ?? DynamicSchemaNameMarker; + + if (!queryStepsByService.TryGetValue(schemaKey, out var serviceSteps)) + { + serviceSteps = []; + queryStepsByService[schemaKey] = serviceSteps; + } + + serviceSteps.Add(operationStep); + } + + if (queryStepsByService.Count == 0) + { + return []; + } + + var dependencyDepthLookup = new Dictionary(); + var recursionStack = new HashSet(); + + foreach (var serviceSteps in queryStepsByService.Values) + { + foreach (var step in serviceSteps) + { + GetDependencyDepth( + step.Id, + dependencyLookup, + dependencyDepthLookup, + recursionStack); + } + } + + var lookup = new Dictionary(); + var nextGroupId = 0; + + foreach (var (schemaKey, serviceSteps) in queryStepsByService.OrderBy(t => t.Key, StringComparer.Ordinal)) + { + var stepsByDepth = new Dictionary>(); + + foreach (var step in serviceSteps) + { + var depth = dependencyDepthLookup.TryGetValue(step.Id, out var currentDepth) + ? currentDepth + : 0; + + if (!stepsByDepth.TryGetValue(depth, out var groupedSteps)) + { + groupedSteps = []; + stepsByDepth.Add(depth, groupedSteps); + } + + groupedSteps.Add(step.Id); + } + + foreach (var groupedSteps in stepsByDepth.OrderBy(t => t.Key).Select(t => t.Value)) + { + if (groupedSteps.Count <= 1) + { + continue; + } + + groupedSteps.Sort(); + var groupId = ++nextGroupId; + + foreach (var stepId in groupedSteps) + { + lookup.Add(stepId, groupId); + } + } + } + + return lookup; + } + + private static int GetDependencyDepth( + int stepId, + Dictionary> dependencyLookup, + Dictionary dependencyDepthLookup, + HashSet recursionStack) + { + if (dependencyDepthLookup.TryGetValue(stepId, out var depth)) + { + return depth; + } + + if (!dependencyLookup.TryGetValue(stepId, out var directDependencies) + || directDependencies.Count == 0) + { + dependencyDepthLookup[stepId] = 0; + return 0; + } + + if (!recursionStack.Add(stepId)) + { + throw new InvalidOperationException("The execution dependency graph contains a cycle."); + } + + var maxDepth = 0; + + foreach (var dependency in directDependencies.OrderBy(t => t)) + { + var dependencyDepth = GetDependencyDepth( + dependency, + dependencyLookup, + dependencyDepthLookup, + recursionStack); + maxDepth = Math.Max(maxDepth, dependencyDepth + 1); + } + + recursionStack.Remove(stepId); + dependencyDepthLookup[stepId] = maxDepth; + return maxDepth; + } + private static void BuildDependencyStructure( Dictionary completedNodes, Dictionary> dependencyLookup, @@ -333,7 +478,8 @@ private static void BuildDependencyStructure( { foreach (var (nodeId, stepDependencies) in dependencyLookup) { - if (!completedNodes.TryGetValue(nodeId, out var entry) || entry is not OperationExecutionNode node) + if (!completedNodes.TryGetValue(nodeId, out var entry) + || entry is not (OperationExecutionNode or OperationBatchExecutionNode)) { continue; } @@ -341,13 +487,13 @@ private static void BuildDependencyStructure( foreach (var dependencyId in stepDependencies) { if (!completedNodes.TryGetValue(dependencyId, out var childEntry) - || childEntry is not (OperationExecutionNode or NodeFieldExecutionNode)) + || childEntry is not (OperationExecutionNode or OperationBatchExecutionNode or NodeFieldExecutionNode)) { continue; } - childEntry.AddDependent(node); - node.AddDependency(childEntry); + childEntry.AddDependent(entry); + entry.AddDependency(childEntry); } } @@ -385,6 +531,234 @@ private static void BuildDependencyStructure( } } + private static void MergeEquivalentOperationNodes( + Dictionary completedNodes, + Dictionary> dependencyLookup) + { + // We group OperationExecutionNodes by (schemaName, sortedDependencies). + // Nodes must have identical dependency sets to be safely mergeable. + // + // A node with different dependencies may be gated behind a conditional branch + // (e.g. NodeField inline-fragment dispatch) that never fires for certain entity types, + // so merging them would create a node whose dependency union can never be fully satisfied, + // possibly causing a deadlock. + var candidates = new Dictionary<(string schema, string deps), List>(); + + foreach (var node in completedNodes.Values.OfType()) + { + var schemaKey = node.SchemaName ?? DynamicSchemaNameMarker; + var depsKey = dependencyLookup.TryGetValue(node.Id, out var depsSet) + ? string.Join(",", depsSet.Order()) + : string.Empty; + var groupKey = (schemaKey, depsKey); + + if (!candidates.TryGetValue(groupKey, out var list)) + { + list = []; + candidates[groupKey] = list; + } + + list.Add(node); + } + + // Within each bucket, find sub-groups with identical canonical signatures and merge them. + foreach (var (_, groupNodes) in candidates) + { + if (groupNodes.Count <= 1) + { + continue; + } + + var bySignature = new Dictionary>(StringComparer.Ordinal); + + foreach (var node in groupNodes) + { + var sig = ComputeCanonicalSignature(node); + + if (!bySignature.TryGetValue(sig, out var sigGroup)) + { + sigGroup = []; + bySignature[sig] = sigGroup; + } + + sigGroup.Add(node); + } + + foreach (var (_, equivalentNodes) in bySignature) + { + if (equivalentNodes.Count <= 1) + { + continue; + } + + // Stable order: lowest ID becomes the canonical node. + equivalentNodes.Sort((a, b) => a.Id.CompareTo(b.Id)); + + var primary = equivalentNodes[0]; + var otherIds = equivalentNodes.Skip(1).Select(n => n.Id).ToList(); + + var (canonicalOp, canonicalRequirements) = CanonicalizeOperation(primary); + var targets = equivalentNodes.Select(n => n.Target).ToArray(); + + var mergedNode = new OperationBatchExecutionNode( + primary.Id, + canonicalOp, + primary.SchemaName, + targets, + primary.Source, + canonicalRequirements, + primary.ForwardedVariables.ToArray(), + primary.ResponseNames.ToArray(), + primary.Conditions.ToArray(), + primary.BatchingGroupId, + primary.RequiresFileUpload); + + completedNodes[primary.Id] = mergedNode; + + foreach (var otherId in otherIds) + { + completedNodes.Remove(otherId); + } + + // Union all dependency sets under the primary ID. + if (!dependencyLookup.TryGetValue(primary.Id, out var primaryDeps)) + { + primaryDeps = []; + } + + foreach (var otherId in otherIds) + { + if (dependencyLookup.TryGetValue(otherId, out var otherDeps)) + { + foreach (var dep in otherDeps) + { + primaryDeps.Add(dep); + } + + dependencyLookup.Remove(otherId); + } + } + + if (primaryDeps.Count > 0) + { + dependencyLookup[primary.Id] = primaryDeps; + } + else + { + dependencyLookup.Remove(primary.Id); + } + + // Replace all references to the removed IDs with the primary ID. + var otherIdSet = new HashSet(otherIds); + + foreach (var depSet in dependencyLookup.Values) + { + var hadOther = false; + + foreach (var otherId in otherIdSet) + { + if (depSet.Remove(otherId)) + { + hadOther = true; + } + } + + if (hadOther) + { + depSet.Add(primary.Id); + } + } + } + } + } + + private static string ComputeCanonicalSignature(OperationExecutionNode node) + { + var replacements = BuildPrefixReplacements(node.Requirements); + var normalizedText = ApplyPrefixReplacements(node.Operation.SourceText, replacements); + + // Skip the first line — it contains the operation name which embeds the step ID. + var firstNewline = normalizedText.IndexOf('\n'); + var bodyText = firstNewline >= 0 ? normalizedText[(firstNewline + 1)..] : normalizedText; + + var conditions = string.Join(",", node.Conditions.ToArray() + .OrderBy(c => c.VariableName) + .Select(c => $"{c.VariableName}:{c.PassingValue}")); + + return $"{node.SchemaName}|{node.Source}|{conditions}|{bodyText}"; + } + + private static (OperationSourceText operation, OperationRequirement[] requirements) CanonicalizeOperation( + OperationExecutionNode node) + { + // Use the primary node's operation and requirements as-is. + // The primary has the lowest ID (and therefore the lowest __fusion_{N}_ prefix numbers), + // which preserves the globally-unique numbering assigned by the planner. + // ComputeCanonicalSignature normalises prefixes only for equivalence comparison, + // but the actual merged operation must keep the original numbers. + return (node.Operation, node.Requirements.ToArray()); + } + + /// + /// Builds a list of (original, canonical) string pairs for normalizing + /// __fusion_{N}_ variable-name prefixes. Prefixes are sorted + /// deterministically by the alphabetically-joined set of their argument names + /// so that structurally identical operations always produce the same mapping. + /// + private static (string original, string canonical)[] BuildPrefixReplacements( + ReadOnlySpan requirements) + { + var prefixToArgs = new Dictionary>(StringComparer.Ordinal); + + foreach (var req in requirements) + { + var key = req.Key; + var lastUnderscore = key.LastIndexOf('_'); + + if (lastUnderscore <= 0) + { + continue; + } + + var prefix = key[..lastUnderscore]; + var arg = key[(lastUnderscore + 1)..]; + + if (!prefixToArgs.TryGetValue(prefix, out var args)) + { + args = new(StringComparer.Ordinal); + prefixToArgs[prefix] = args; + } + + args.Add(arg); + } + + var sortedPrefixes = prefixToArgs + .OrderBy(kvp => string.Join(",", kvp.Value), StringComparer.Ordinal) + .Select(kvp => kvp.Key) + .ToList(); + + var result = new (string original, string canonical)[sortedPrefixes.Count]; + + for (var i = 0; i < sortedPrefixes.Count; i++) + { + result[i] = ($"{sortedPrefixes[i]}_", $"__fusion_{i}_"); + } + + return result; + } + + private static string ApplyPrefixReplacements( + string text, + ReadOnlySpan<(string original, string canonical)> replacements) + { + foreach (var (original, canonical) in replacements) + { + text = text.Replace(original, canonical); + } + + return text; + } + private static string[] GetResponseNamesFromPath( OperationDefinitionNode operationDefinition, SelectionPath path) @@ -534,7 +908,7 @@ private static OperationDefinitionNode RemoveEmptySelections(OperationDefinition { var selection = selections[i]; var removeSelection = - selection is FieldNode { SelectionSet: { Selections.Count: 0 } } + selection is FieldNode { SelectionSet.Selections.Count: 0 } || selection is InlineFragmentNode { SelectionSet.Selections.Count: 0 }; if (!removeSelection) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlannerOptions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlannerOptions.cs index 9f6e3a9214b..9ba2ee71dbc 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlannerOptions.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Planning/OperationPlannerOptions.cs @@ -42,6 +42,21 @@ public double OperationWeight } } = 1.5; + /// + /// Gets or sets whether the planner assigns batching group IDs to execution nodes. + /// When enabled, independent query operations targeting the same source schema are + /// grouped so the executor can dispatch them as a single transport-level batch request. + /// + public bool EnableRequestGrouping + { + get; + set + { + ExpectMutableOptions(); + field = value; + } + } = true; + /// /// Gets or sets the weight applied for each operation beyond the fan-out penalty threshold. /// diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Properties/FusionExecutionResources.Designer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Properties/FusionExecutionResources.Designer.cs index 8c747592ab9..d276e60b333 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Properties/FusionExecutionResources.Designer.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Properties/FusionExecutionResources.Designer.cs @@ -92,5 +92,65 @@ internal static string OperationPlannerGuardrailException_GuardrailExceeded { return ResourceManager.GetString("OperationPlannerGuardrailException_GuardrailExceeded", resourceCulture); } } + + internal static string SourceSchemaRequestDispatcher_NodeNotRegisteredInGroup { + get { + return ResourceManager.GetString("SourceSchemaRequestDispatcher_NodeNotRegisteredInGroup", resourceCulture); + } + } + + internal static string SourceSchemaHttpClient_SubscriptionBatchNotSupported { + get { + return ResourceManager.GetString("SourceSchemaHttpClient_SubscriptionBatchNotSupported", resourceCulture); + } + } + + internal static string SourceSchemaHttpClient_InvalidRequestIndex { + get { + return ResourceManager.GetString("SourceSchemaHttpClient_InvalidRequestIndex", resourceCulture); + } + } + + internal static string SourceSchemaHttpClient_NoResponseChannelForNode { + get { + return ResourceManager.GetString("SourceSchemaHttpClient_NoResponseChannelForNode", resourceCulture); + } + } + + internal static string SourceSchemaHttpClient_InvalidVariableIndex { + get { + return ResourceManager.GetString("SourceSchemaHttpClient_InvalidVariableIndex", resourceCulture); + } + } + + internal static string SourceSchemaHttpClient_NoResultForNode { + get { + return ResourceManager.GetString("SourceSchemaHttpClient_NoResultForNode", resourceCulture); + } + } + + internal static string SourceSchemaRequestDispatcher_DuplicateNodeSubmission { + get { + return ResourceManager.GetString("SourceSchemaRequestDispatcher_DuplicateNodeSubmission", resourceCulture); + } + } + + internal static string SourceSchemaRequestDispatcher_RegisterGroupEmptyNodeIds { + get { + return ResourceManager.GetString("SourceSchemaRequestDispatcher_RegisterGroupEmptyNodeIds", resourceCulture); + } + } + + internal static string SourceSchemaRequestDispatcher_OperationAborted { + get { + return ResourceManager.GetString("SourceSchemaRequestDispatcher_OperationAborted", resourceCulture); + } + } + + internal static string SourceSchemaRequestDispatcher_BatchResponseCountMismatch { + get { + return ResourceManager.GetString("SourceSchemaRequestDispatcher_BatchResponseCountMismatch", resourceCulture); + } + } } } diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Properties/FusionExecutionResources.resx b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Properties/FusionExecutionResources.resx index 076a67181c4..a5c276b9ef4 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Properties/FusionExecutionResources.resx +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Execution/Properties/FusionExecutionResources.resx @@ -42,4 +42,34 @@ Planner guardrail exceeded for operation '{0}': {1} (Limit={2}, Observed={3}). + + The execution node with id '{0}' is registered to be part of batching group '{1}', but no request was submitted for that node. + + + Subscription requests are not supported by batch execution. + + + The batch response contains an invalid requestIndex '{0}'. + + + No response channel exists for node '{0}'. + + + The batch response contains an invalid variableIndex '{0}' for node '{1}'. + + + The batch response does not contain any result for node '{0}'. + + + Node '{0}' was already submitted to its batching group. + + + At least one node ID must be provided. + + + The operation execution was aborted. + + + The client did not return a response for each request in the batch. + diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/FusionTestBase.CreateSourceSchema.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/FusionTestBase.CreateSourceSchema.cs index 339633c6ac3..2675b0f6429 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/FusionTestBase.CreateSourceSchema.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/FusionTestBase.CreateSourceSchema.cs @@ -20,7 +20,9 @@ protected TestServer CreateSourceSchema( Action? configureServices = null, Action? configureApplication = null, Action? configureHttpClient = null, - SourceSchemaHttpClientBatchingMode batchingMode = SourceSchemaHttpClientBatchingMode.VariableBatching, + SourceSchemaHttpClientBatchingMode batchingMode = + SourceSchemaHttpClientBatchingMode.VariableBatching + | SourceSchemaHttpClientBatchingMode.RequestBatching, ImmutableArray? batchingAcceptHeaderValues = null, bool isOffline = false, bool isTimingOut = false) @@ -61,7 +63,9 @@ protected TestServer CreateSourceSchema( string schemaText, bool isOffline = false, bool isTimingOut = false, - SourceSchemaHttpClientBatchingMode batchingMode = SourceSchemaHttpClientBatchingMode.VariableBatching, + SourceSchemaHttpClientBatchingMode batchingMode = + SourceSchemaHttpClientBatchingMode.VariableBatching + | SourceSchemaHttpClientBatchingMode.RequestBatching, ImmutableArray? batchingAcceptHeaderValues = null, Action? configureHttpClient = null, HttpClient? httpClient = null) diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/FusionTestBase.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/FusionTestBase.cs index 2effdcdadf7..7cf95924350 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/FusionTestBase.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/FusionTestBase.cs @@ -172,9 +172,7 @@ protected async Task CreateCompositeSchemaAsync( SourceSchemaInteraction GetSourceSchemaInteraction(OperationPlanContext context, ExecutionNode node) { - var schemaName = node is OperationExecutionNode { SchemaName: { } staticSchemaName } - ? staticSchemaName - : context.GetDynamicSchemaName(node); + var schemaName = node.SchemaName ?? context.GetDynamicSchemaName(node); var schemaInteractions = interactions.GetOrAdd(schemaName, _ => []); return schemaInteractions.GetOrAdd(node.Id, _ => new SourceSchemaInteraction()); diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/K6ExecutionTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/K6ExecutionTests.cs new file mode 100644 index 00000000000..856dbacfa11 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/K6ExecutionTests.cs @@ -0,0 +1,326 @@ +using System.Text.Json; +using HotChocolate; +using HotChocolate.Transport.Http; +using HotChocolate.Types.Composite; +using HotChocolate.Types.Relay; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Fusion; + +public sealed class K6ExecutionTests : FusionTestBase +{ + [Fact] + public async Task Execute_K6Query_With_RequestGrouping_Enabled_Completes_Without_Errors() + { + // arrange + using var accountsApi = CreateSourceSchema( + "accounts-api", + builder => builder.AddQueryType(d => d.Name("Query"))); + + using var inventoryApi = CreateSourceSchema( + "inventory-api", + builder => builder.AddQueryType(d => d.Name("Query"))); + + using var productsApi = CreateSourceSchema( + "products-api", + builder => builder.AddQueryType(d => d.Name("Query"))); + + using var reviewsApi = CreateSourceSchema( + "reviews-api", + builder => builder.AddQueryType(d => d.Name("Query"))); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("accounts-api", accountsApi), + ("inventory-api", inventoryApi), + ("products-api", productsApi), + ("reviews-api", reviewsApi) + ], + configureGatewayBuilder: builder => + builder.ModifyPlannerOptions(options => options.EnableRequestGrouping = true)); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + // act + using var result = await client.PostAsync( + K6Query, + new Uri("http://localhost:5000/graphql")) + .WaitAsync(TimeSpan.FromSeconds(10)); + + using var response = await result.ReadAsResultAsync(); + + // assert + Assert.NotEqual(JsonValueKind.Array, response.Errors.ValueKind); + Assert.Equal(JsonValueKind.Object, response.Data.ValueKind); + + var users = response.Data.GetProperty("users"); + Assert.Equal(JsonValueKind.Array, users.ValueKind); + Assert.True(users.GetArrayLength() > 0); + + var topProducts = response.Data.GetProperty("topProducts"); + Assert.Equal(JsonValueKind.Array, topProducts.ValueKind); + Assert.True(topProducts.GetArrayLength() > 0); + + var topProductReviews = topProducts[0].GetProperty("reviews"); + Assert.Equal(JsonValueKind.Array, topProductReviews.ValueKind); + Assert.True(topProductReviews.GetArrayLength() > 0); + } + + public sealed class AccountsQuery + { + private static readonly List s_users = + [ + new() { Id = "1", Name = "Uri Goldshtein", Username = "urigo", Birthday = 1234567890 }, + new() { Id = "2", Name = "Dotan Simha", Username = "dotansimha", Birthday = 1234567890 }, + new() { Id = "3", Name = "Kamil Kisiela", Username = "kamilkisiela", Birthday = 1234567890 }, + new() { Id = "4", Name = "Arda Tanrikulu", Username = "ardatan", Birthday = 1234567890 }, + new() { Id = "5", Name = "Gil Gardosh", Username = "gilgardosh", Birthday = 1234567890 }, + new() { Id = "6", Name = "Laurin Quast", Username = "laurin", Birthday = 1234567890 } + ]; + + public AccountUser? GetMe() => s_users[0]; + + [Lookup] + public AccountUser? GetUser([ID] string id) + => s_users.FirstOrDefault(u => u.Id == id); + + public List GetUsers() => s_users; + } + + [GraphQLName("User")] + public sealed class AccountUser + { + [ID] + public required string Id { get; init; } + + public string? Name { get; init; } + + public string? Username { get; init; } + + public int? Birthday { get; init; } + } + + public sealed class ProductsQuery + { + private static readonly List s_products = + [ + new() { Upc = "1", Name = "Table", Price = 899, Weight = 100 }, + new() { Upc = "2", Name = "Couch", Price = 1299, Weight = 1000 }, + new() { Upc = "3", Name = "Glass", Price = 15, Weight = 20 }, + new() { Upc = "4", Name = "Chair", Price = 499, Weight = 100 }, + new() { Upc = "5", Name = "TV", Price = 1299, Weight = 1000 }, + new() { Upc = "6", Name = "Lamp", Price = 6999, Weight = 300 }, + new() { Upc = "7", Name = "Grill", Price = 3999, Weight = 2000 }, + new() { Upc = "8", Name = "Fridge", Price = 100000, Weight = 6000 }, + new() { Upc = "9", Name = "Sofa", Price = 9999, Weight = 800 } + ]; + + public IEnumerable GetTopProducts(int first = 5) + => s_products.Take(first); + + [Lookup] + public ProductModel? GetProduct([ID] string upc) + => s_products.FirstOrDefault(p => p.Upc == upc); + } + + [GraphQLName("Product")] + public sealed class ProductModel + { + [ID] + public required string Upc { get; init; } + + public required string Name { get; init; } + + public long Price { get; init; } + + public long Weight { get; init; } + } + + public sealed class InventoryQuery + { + private static readonly Dictionary s_inventory = new() + { + { "1", new InventoryProduct { Upc = "1", InStock = true } }, + { "2", new InventoryProduct { Upc = "2", InStock = false } }, + { "3", new InventoryProduct { Upc = "3", InStock = false } }, + { "4", new InventoryProduct { Upc = "4", InStock = false } }, + { "5", new InventoryProduct { Upc = "5", InStock = true } }, + { "6", new InventoryProduct { Upc = "6", InStock = true } }, + { "7", new InventoryProduct { Upc = "7", InStock = true } }, + { "8", new InventoryProduct { Upc = "8", InStock = false } }, + { "9", new InventoryProduct { Upc = "9", InStock = true } } + }; + + [Lookup, Internal] + public InventoryProduct? GetProductByUpc([ID] string upc) + { + s_inventory.TryGetValue(upc, out var product); + return product; + } + } + + [GraphQLName("Product")] + public sealed class InventoryProduct + { + [ID] + public required string Upc { get; init; } + + public bool InStock { get; init; } + + public long? GetShippingEstimate([Require] long weight, [Require] long price) + => price > 1000 ? 0 : weight / 2; + } + + public sealed class ReviewsQuery + { + [Lookup] + public Review? GetReview([ID] string id) + => ReviewRepository.GetById(id); + + [Lookup, Internal] + public ReviewProduct GetProduct([ID] string upc) + => new() { Upc = upc }; + + [Lookup, Internal] + public ReviewUser GetUser([ID] string id) + => new() { Id = id }; + } + + [GraphQLName("User")] + public sealed class ReviewUser + { + [ID] + public required string Id { get; init; } + + public IEnumerable GetReviews() + => ReviewRepository.GetByUserId(Id); + } + + [GraphQLName("Product")] + public sealed class ReviewProduct + { + [ID] + public required string Upc { get; init; } + + public IEnumerable GetReviews() + => ReviewRepository.GetByProductUpc(Upc); + } + + public sealed class Review + { + [ID] + public required string Id { get; init; } + + public required string Body { get; init; } + + public required string AuthorId { get; init; } + + public required string ProductUpc { get; init; } + + public ReviewUser GetAuthor() + => new() { Id = AuthorId }; + + public ReviewProduct GetProduct() + => new() { Upc = ProductUpc }; + } + + public static class ReviewRepository + { + private static readonly List s_reviews = + [ + new() { Id = "1", AuthorId = "1", ProductUpc = "1", Body = "Review 1" }, + new() { Id = "2", AuthorId = "1", ProductUpc = "1", Body = "Review 2" }, + new() { Id = "3", AuthorId = "1", ProductUpc = "1", Body = "Review 3" }, + new() { Id = "4", AuthorId = "1", ProductUpc = "1", Body = "Review 4" }, + new() { Id = "5", AuthorId = "1", ProductUpc = "2", Body = "Review 5" }, + new() { Id = "6", AuthorId = "1", ProductUpc = "2", Body = "Review 6" }, + new() { Id = "7", AuthorId = "1", ProductUpc = "2", Body = "Review 7" }, + new() { Id = "8", AuthorId = "1", ProductUpc = "2", Body = "Review 8" }, + new() { Id = "9", AuthorId = "1", ProductUpc = "3", Body = "Review 9" }, + new() { Id = "10", AuthorId = "1", ProductUpc = "4", Body = "Review 10" }, + new() { Id = "11", AuthorId = "1", ProductUpc = "4", Body = "Review 11" } + ]; + + public static IEnumerable GetByUserId(string authorId) + => s_reviews.Take(2); + + public static IEnumerable GetByProductUpc(string upc) + => s_reviews.Where(r => r.ProductUpc == upc); + + public static Review? GetById(string id) + => s_reviews.FirstOrDefault(r => r.Id == id); + } + + private const string K6Query = + """ + query TestQuery { + users { + id + username + name + reviews { + id + body + product { + inStock + name + price + shippingEstimate + upc + weight + reviews { + id + body + author { + id + username + name + reviews { + id + body + product { + inStock + name + price + shippingEstimate + upc + weight + } + } + } + } + } + } + } + topProducts(first: 5) { + inStock + name + price + shippingEstimate + upc + weight + reviews { + id + body + author { + id + username + name + reviews { + id + body + product { + inStock + name + price + shippingEstimate + upc + weight + } + } + } + } + } + } + """; +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/RequestGroupingExecutionTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/RequestGroupingExecutionTests.cs new file mode 100644 index 00000000000..91a6d7f70e4 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/RequestGroupingExecutionTests.cs @@ -0,0 +1,292 @@ +using System.Collections.Concurrent; +using System.Text.Json; +using HotChocolate.Transport.Http; +using HotChocolate.Types.Composite; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Fusion; + +public sealed class RequestGroupingExecutionTests : FusionTestBase +{ + [Fact] + public async Task Execute_With_RequestGrouping_Enabled_Coalesces_Lookup_Requests() + { + // arrange + using var serverA = CreateSourceSchema( + "a", + builder => builder.AddQueryType()); + + using var serverB = CreateSourceSchema( + "b", + builder => builder.AddQueryType()); + + using var serverC = CreateSourceSchema( + "c", + builder => builder.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", serverA), + ("b", serverB), + ("c", serverC) + ], + configureGatewayBuilder: builder => + builder.ModifyPlannerOptions(options => options.EnableRequestGrouping = true)); + + // act + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + using var result = await client.PostAsync( + """ + { + first { + id + rating + deliveryEstimate + } + second { + id + rating + deliveryEstimate + } + } + """, + new Uri("http://localhost:5000/graphql")); + + using var response = await result.ReadAsResultAsync(); + + // assert + Assert.Equal(JsonValueKind.Object, response.Data.ValueKind); + + var first = response.Data.GetProperty("first"); + Assert.Equal(1, first.GetProperty("id").GetInt32()); + Assert.Equal(5, first.GetProperty("rating").GetInt32()); + Assert.Equal(2, first.GetProperty("deliveryEstimate").GetInt32()); + + var second = response.Data.GetProperty("second"); + Assert.Equal(2, second.GetProperty("id").GetInt32()); + Assert.Equal(4, second.GetProperty("rating").GetInt32()); + Assert.Equal(3, second.GetProperty("deliveryEstimate").GetInt32()); + + var bInteractions = AssertSchemaInteractions(gateway.Interactions, "b"); + var cInteractions = AssertSchemaInteractions(gateway.Interactions, "c"); + + AssertAllRequestsAreVariableBatches(bInteractions, expectedVariablesCount: 2); + AssertAllRequestsAreVariableBatches(cInteractions, expectedVariablesCount: 2); + } + + [Fact] + public async Task Execute_With_RequestGrouping_Enabled_Does_Not_Deadlock_Across_Depths() + { + // arrange + using var serverA = CreateSourceSchema( + "a", + builder => builder.AddQueryType()); + + using var serverC = CreateSourceSchema( + "c", + builder => builder.AddQueryType()); + + using var serverD = CreateSourceSchema( + "d", + builder => builder.AddQueryType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("a", serverA), + ("c", serverC), + ("d", serverD) + ], + configureGatewayBuilder: builder => + builder.ModifyPlannerOptions(options => options.EnableRequestGrouping = true)); + + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + // act + using var result = await client.PostAsync( + """ + { + users { + id + reviews { + id + author { + id + name + } + } + } + topProducts { + id + reviews { + id + author { + id + name + } + } + } + } + """, + new Uri("http://localhost:5000/graphql")).WaitAsync(TimeSpan.FromSeconds(5)); + + using var response = await result.ReadAsResultAsync(); + + // assert + Assert.Equal(JsonValueKind.Object, response.Data.ValueKind); + + var users = response.Data.GetProperty("users"); + Assert.Equal(JsonValueKind.Array, users.ValueKind); + Assert.True(users.GetArrayLength() > 0); + + var firstUser = users[0]; + var firstUserReview = firstUser.GetProperty("reviews")[0]; + var firstAuthor = firstUserReview.GetProperty("author"); + var firstAuthorId = firstAuthor.GetProperty("id").GetInt32(); + Assert.Equal($"User {firstAuthorId + 100}", firstAuthor.GetProperty("name").GetString()); + + var topProducts = response.Data.GetProperty("topProducts"); + Assert.Equal(JsonValueKind.Array, topProducts.ValueKind); + Assert.True(topProducts.GetArrayLength() > 0); + Assert.Equal(JsonValueKind.Array, topProducts[0].GetProperty("reviews").ValueKind); + } + + private static ConcurrentDictionary AssertSchemaInteractions( + ConcurrentDictionary> interactions, + string schemaName) + { + Assert.True(interactions.TryGetValue(schemaName, out var schemaInteractions)); + Assert.NotNull(schemaInteractions); + // Equivalent operations are merged into a single OperationBatchExecutionNode + // that uses variable batching, so there is one interaction per schema. + Assert.Single(schemaInteractions); + return schemaInteractions; + } + + private static void AssertAllRequestsAreVariableBatches( + ConcurrentDictionary interactions, + int expectedVariablesCount) + { + foreach (var interaction in interactions.Values) + { + Assert.NotNull(interaction.Request); + var request = interaction.Request!; + request.Body.Position = 0; + + using var body = JsonDocument.Parse(request.Body); + Assert.Equal(JsonValueKind.Object, body.RootElement.ValueKind); + Assert.True(body.RootElement.TryGetProperty("variables", out var variables)); + Assert.Equal(JsonValueKind.Array, variables.ValueKind); + Assert.Equal(expectedVariablesCount, variables.GetArrayLength()); + } + } + + public static class SourceSchemaA + { + [EntityKey("id")] + public record Product(int Id); + + public sealed class Query + { + public Product GetFirst() => new(1); + + public Product GetSecond() => new(2); + } + } + + public static class SourceSchemaB + { + private static readonly Dictionary s_products = new() + { + [1] = new Product(1, 5), + [2] = new Product(2, 4) + }; + + [EntityKey("id")] + public record Product(int Id, int Rating); + + public sealed class Query + { + [Lookup] + [Internal] + public Product GetProductById(int id) => s_products[id]; + } + } + + public static class SourceSchemaC + { + private static readonly Dictionary s_products = new() + { + [1] = new Product(1, 2), + [2] = new Product(2, 3) + }; + + [EntityKey("id")] + public record Product(int Id, int DeliveryEstimate); + + public sealed class Query + { + [Lookup] + [Internal] + public Product GetProductById(int id) => s_products[id]; + } + } + + public static class DeadlockSourceSchemaA + { + [EntityKey("id")] + public record User(int Id, string Name); + + public sealed class Query + { + public IReadOnlyList GetUsers() => [new(1, "User 1")]; + + [Lookup] + [Internal] + public User GetUserById(int id) => new(id, $"User {id + 100}"); + } + } + + public static class DeadlockSourceSchemaC + { + [EntityKey("id")] + public record Product(int Id); + + public sealed class Query + { + public IReadOnlyList GetTopProducts() => [new(10)]; + + [Lookup] + [Internal] + public Product GetProductById(int id) => new(id); + } + } + + public static class DeadlockSourceSchemaD + { + [EntityKey("id")] + public record User(int Id) + { + public IReadOnlyList Reviews => [new(Id * 10, new User(Id + 1))]; + } + + [EntityKey("id")] + public record Product(int Id) + { + public IReadOnlyList Reviews => [new(Id * 10, new User(Id + 2))]; + } + + public record Review(int Id, User Author); + + public sealed class Query + { + [Lookup] + [Internal] + public User GetUserById(int id) => new(id); + + [Lookup] + [Internal] + public Product GetProductById(int id) => new(id); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Interface_Type_Refinement.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Interface_Type_Refinement.yaml index e6cf609b576..9a4c0902b97 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Interface_Type_Refinement.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_Around_Interface_Type_Refinement.yaml @@ -164,6 +164,7 @@ operationPlan: } } } + batchingGroupId: 1 forwardedVariables: - skip dependencies: @@ -188,6 +189,7 @@ operationPlan: } } } + batchingGroupId: 1 forwardedVariables: - skip dependencies: diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Interface_Type_Refinement.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Interface_Type_Refinement.yaml index ca86abe8ba7..6d2048ee1ac 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Interface_Type_Refinement.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/ConditionalTests.NodeField_Skip_On_Interface_Type_Refinement.yaml @@ -133,6 +133,7 @@ operationPlan: } } } + batchingGroupId: 1 forwardedVariables: - skip dependencies: @@ -154,6 +155,7 @@ operationPlan: } } } + batchingGroupId: 1 forwardedVariables: - skip dependencies: diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface.yaml index 7d373b2ccfb..9c20f62fcbc 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface.yaml @@ -136,6 +136,7 @@ operationPlan: } } } + batchingGroupId: 1 forwardedVariables: - id dependencies: @@ -155,6 +156,7 @@ operationPlan: } } } + batchingGroupId: 1 forwardedVariables: - id dependencies: diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type.yaml index e1ffe14be58..0c1ab987ebf 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type.yaml @@ -146,6 +146,7 @@ operationPlan: } } } + batchingGroupId: 1 forwardedVariables: - id dependencies: @@ -165,6 +166,7 @@ operationPlan: } } } + batchingGroupId: 1 forwardedVariables: - id dependencies: diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Different_Dependencies.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Different_Dependencies.yaml index 4452339dfaf..09c8f16d104 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Different_Dependencies.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Different_Dependencies.yaml @@ -164,48 +164,33 @@ sourceSchemas: } } } - variables: | - { - "__fusion_1_id": "UHJvZHVjdDo0" - } - response: - results: - - | - { - "data": { - "node": { - "__typename": "Product", - "name": "Product: UHJvZHVjdDo0" - } - } - } - - request: - document: | - query testQuery_f7a0a31d_5( - $__fusion_2_id: ID! - ) { - node(id: $__fusion_2_id) { - __typename - ... on Product { - name - } - } - } variables: | [ { - "__fusion_2_id": "UHJvZHVjdDox" + "__fusion_1_id": "UHJvZHVjdDo0" }, { - "__fusion_2_id": "UHJvZHVjdDoy" + "__fusion_1_id": "UHJvZHVjdDox" }, { - "__fusion_2_id": "UHJvZHVjdDoz" + "__fusion_1_id": "UHJvZHVjdDoy" + }, + { + "__fusion_1_id": "UHJvZHVjdDoz" } ] response: contentType: application/jsonl; charset=utf-8 results: + - | + { + "data": { + "node": { + "__typename": "Product", + "name": "Product: UHJvZHVjdDo0" + } + } + } - | { "data": { @@ -306,12 +291,13 @@ operationPlan: } } } + batchingGroupId: 1 forwardedVariables: - id dependencies: - id: 1 - id: 4 - type: Operation + type: OperationBatch schema: B operation: | query testQuery_f7a0a31d_4( @@ -325,35 +311,16 @@ operationPlan: } } source: $.node - target: $.node.singularProduct + targets: + - $.node.singularProduct + - $.node.products + batchingGroupId: 2 requirements: - name: __fusion_1_id selectionMap: >- id dependencies: - id: 3 - - id: 5 - type: Operation - schema: B - operation: | - query testQuery_f7a0a31d_5( - $__fusion_2_id: ID! - ) { - node(id: $__fusion_2_id) { - __typename - ... on Product { - name - } - } - } - source: $.node - target: $.node.products - requirements: - - name: __fusion_2_id - selectionMap: >- - id - dependencies: - - id: 3 - id: 6 type: Operation schema: A @@ -372,6 +339,7 @@ operationPlan: } } } + batchingGroupId: 1 forwardedVariables: - id dependencies: @@ -392,6 +360,7 @@ operationPlan: } source: $.node target: $.node.products + batchingGroupId: 2 requirements: - name: __fusion_3_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Same_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Same_Dependency.yaml index b82eacf9d26..9953ba1e61c 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Same_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_And_Concrete_Type_Both_Have_Same_Dependency.yaml @@ -120,19 +120,34 @@ sourceSchemas: } interactions: - request: - document: | - query testQuery_648537b2_4( - $__fusion_1_id: ID! - ) { - authorById(id: $__fusion_1_id) { - rating - } - } - variables: | - { - "__fusion_1_id": "QXV0aG9yOjE=" - } + kind: OperationBatch + items: + - document: | + query testQuery_648537b2_4( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + rating + } + } + variables: | + { + "__fusion_1_id": "QXV0aG9yOjE=" + } + - document: | + query testQuery_648537b2_5( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + username + } + } + variables: | + { + "__fusion_2_id": "QXV0aG9yOjE=" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -143,19 +158,34 @@ sourceSchemas: } } - request: - document: | - query testQuery_648537b2_5( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - username - } - } - variables: | - { - "__fusion_2_id": "QXV0aG9yOjE=" - } + kind: OperationBatch + items: + - document: | + query testQuery_648537b2_4( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + rating + } + } + variables: | + { + "__fusion_1_id": "QXV0aG9yOjE=" + } + - document: | + query testQuery_648537b2_5( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + username + } + } + variables: | + { + "__fusion_2_id": "QXV0aG9yOjE=" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -253,6 +283,7 @@ operationPlan: } source: $.authorById target: $.node.author + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -272,6 +303,7 @@ operationPlan: } source: $.authorById target: $.node.author + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_Selection_Has_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_Selection_Has_Dependency.yaml index a1c8ae78361..ab68ea140ec 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_Selection_Has_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Selections_On_Interface_Selection_Has_Dependency.yaml @@ -250,6 +250,7 @@ operationPlan: } } } + batchingGroupId: 1 forwardedVariables: - id dependencies: @@ -270,6 +271,7 @@ operationPlan: } source: $.node target: $.node.products + batchingGroupId: 2 requirements: - name: __fusion_1_id selectionMap: >- @@ -294,6 +296,7 @@ operationPlan: } } } + batchingGroupId: 1 forwardedVariables: - id dependencies: @@ -314,6 +317,7 @@ operationPlan: } source: $.node target: $.node.products + batchingGroupId: 2 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Two_Concrete_Types_Selections_Have_Different_Dependencies.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Two_Concrete_Types_Selections_Have_Different_Dependencies.yaml index 5f9bc0b5e49..4bf9da4c68e 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Two_Concrete_Types_Selections_Have_Different_Dependencies.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Two_Concrete_Types_Selections_Have_Different_Dependencies.yaml @@ -245,6 +245,7 @@ operationPlan: } source: $.node target: $.node.product + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -288,6 +289,7 @@ operationPlan: } source: $.node target: $.node.product + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Two_Concrete_Types_Selections_Have_Same_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Two_Concrete_Types_Selections_Have_Same_Dependency.yaml index 3f409803dd6..331a20d4ec7 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Two_Concrete_Types_Selections_Have_Same_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Node_Field_Two_Concrete_Types_Selections_Have_Same_Dependency.yaml @@ -242,6 +242,7 @@ operationPlan: } source: $.node target: $.node.product + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -285,6 +286,7 @@ operationPlan: } source: $.node target: $.node.product + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Two_Node_Fields_With_Alias.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Two_Node_Fields_With_Alias.yaml index e349c26ce55..a3cf82f09a2 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Two_Node_Fields_With_Alias.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/GlobalObjectIdentificationTests.Two_Node_Fields_With_Alias.yaml @@ -49,15 +49,25 @@ sourceSchemas: } interactions: - request: - document: | - query Op_a361113f_3 { - b: discussionById(discussionId: "RGlzY3Vzc2lvbjoy") { - __typename - title - id - } - } + kind: OperationBatch + items: + - document: | + query Op_a361113f_3 { + b: discussionById(discussionId: "RGlzY3Vzc2lvbjoy") { + __typename + title + id + } + } + - document: | + query Op_a361113f_7 { + a: discussionById(discussionId: "RGlzY3Vzc2lvbjox") { + __typename + title + } + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -70,14 +80,25 @@ sourceSchemas: } } - request: - document: | - query Op_a361113f_7 { - a: discussionById(discussionId: "RGlzY3Vzc2lvbjox") { - __typename - title - } - } + kind: OperationBatch + items: + - document: | + query Op_a361113f_3 { + b: discussionById(discussionId: "RGlzY3Vzc2lvbjoy") { + __typename + title + id + } + } + - document: | + query Op_a361113f_7 { + a: discussionById(discussionId: "RGlzY3Vzc2lvbjox") { + __typename + title + } + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -169,6 +190,7 @@ operationPlan: __typename } } + batchingGroupId: 2 dependencies: - id: 1 - id: 3 @@ -182,6 +204,7 @@ operationPlan: id } } + batchingGroupId: 1 dependencies: - id: 1 - id: 4 @@ -218,6 +241,7 @@ operationPlan: __typename } } + batchingGroupId: 2 dependencies: - id: 5 - id: 7 @@ -230,5 +254,6 @@ operationPlan: title } } + batchingGroupId: 1 dependencies: - id: 5 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml index e07a3fdb538..a5d02327aab 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml @@ -99,19 +99,34 @@ sourceSchemas: } interactions: - request: - document: | - query testQuery_1703e95b_2( - $__fusion_1_id: ID! - ) { - authorById(id: $__fusion_1_id) { - email - } - } - variables: | - { - "__fusion_1_id": "QXV0aG9yOjI=" - } + kind: OperationBatch + items: + - document: | + query testQuery_1703e95b_2( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + email + } + } + variables: | + { + "__fusion_1_id": "QXV0aG9yOjI=" + } + - document: | + query testQuery_1703e95b_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + displayName + } + } + variables: | + { + "__fusion_2_id": "QXV0aG9yOjI=" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -122,19 +137,34 @@ sourceSchemas: } } - request: - document: | - query testQuery_1703e95b_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - displayName - } - } - variables: | - { - "__fusion_2_id": "QXV0aG9yOjI=" - } + kind: OperationBatch + items: + - document: | + query testQuery_1703e95b_2( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + email + } + } + variables: | + { + "__fusion_1_id": "QXV0aG9yOjI=" + } + - document: | + query testQuery_1703e95b_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + displayName + } + } + variables: | + { + "__fusion_2_id": "QXV0aG9yOjI=" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -198,6 +228,7 @@ operationPlan: } source: $.authorById target: $.authorable.author + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -217,6 +248,7 @@ operationPlan: } source: $.authorById target: $.authorable.author + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml index 3df452c2461..fc446438829 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml @@ -120,21 +120,6 @@ sourceSchemas: } } } - - request: - document: | - query testQuery_77a61d25_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - displayName - } - } - variables: | - { - "__fusion_2_id": "QXV0aG9yOjI=" - } - response: - results: - | { "data": { @@ -186,7 +171,7 @@ operationPlan: } } - id: 2 - type: Operation + type: OperationBatch schema: B operation: | query testQuery_77a61d25_2( @@ -197,29 +182,13 @@ operationPlan: } } source: $.authorById - target: $.authorable.author + targets: + - $.authorable.author + - $.authorable.author + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- id dependencies: - id: 1 - - id: 3 - type: Operation - schema: B - operation: | - query testQuery_77a61d25_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - displayName - } - } - source: $.authorById - target: $.authorable.author - requirements: - - name: __fusion_2_id - selectionMap: >- - id - dependencies: - - id: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml index a70cc400f4c..895e94ec4d6 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml @@ -128,23 +128,45 @@ sourceSchemas: } interactions: - request: - document: | - query testQuery_e4ba4706_2( - $__fusion_1_id: ID! - ) { - authorById(id: $__fusion_1_id) { - email - } - } - variables: | - [ - { - "__fusion_1_id": "QXV0aG9yOjQ=" - }, - { - "__fusion_1_id": "QXV0aG9yOjY=" - } - ] + kind: OperationBatch + items: + - document: | + query testQuery_e4ba4706_2( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + email + } + } + variables: | + [ + { + "__fusion_1_id": "QXV0aG9yOjQ=" + }, + { + "__fusion_1_id": "QXV0aG9yOjY=" + } + ] + - document: | + query testQuery_e4ba4706_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + displayName + } + } + variables: | + [ + { + "__fusion_2_id": "QXV0aG9yOjQ=" + }, + { + "__fusion_2_id": "QXV0aG9yOjU=" + }, + { + "__fusion_2_id": "QXV0aG9yOjY=" + } + ] response: contentType: application/jsonl; charset=utf-8 results: @@ -152,7 +174,7 @@ sourceSchemas: { "data": { "authorById": { - "email": "Author: QXV0aG9yOjQ=" + "email": "Author: QXV0aG9yOjY=" } } } @@ -160,31 +182,50 @@ sourceSchemas: { "data": { "authorById": { - "email": "Author: QXV0aG9yOjY=" + "email": "Author: QXV0aG9yOjQ=" } } } - request: - document: | - query testQuery_e4ba4706_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - displayName - } - } - variables: | - [ - { - "__fusion_2_id": "QXV0aG9yOjQ=" - }, - { - "__fusion_2_id": "QXV0aG9yOjU=" - }, - { - "__fusion_2_id": "QXV0aG9yOjY=" - } - ] + kind: OperationBatch + items: + - document: | + query testQuery_e4ba4706_2( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + email + } + } + variables: | + [ + { + "__fusion_1_id": "QXV0aG9yOjQ=" + }, + { + "__fusion_1_id": "QXV0aG9yOjY=" + } + ] + - document: | + query testQuery_e4ba4706_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + displayName + } + } + variables: | + [ + { + "__fusion_2_id": "QXV0aG9yOjQ=" + }, + { + "__fusion_2_id": "QXV0aG9yOjU=" + }, + { + "__fusion_2_id": "QXV0aG9yOjY=" + } + ] response: contentType: application/jsonl; charset=utf-8 results: @@ -192,7 +233,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjU=" } } } @@ -200,7 +241,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjU=" + "displayName": "Author: QXV0aG9yOjY=" } } } @@ -208,7 +249,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjY=" + "displayName": "Author: QXV0aG9yOjQ=" } } } @@ -266,6 +307,7 @@ operationPlan: } source: $.authorById target: $.authorables.author + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -285,6 +327,7 @@ operationPlan: } source: $.authorById target: $.authorables.author + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml index c39f8f77e75..cd60b57b6a9 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.Interface_List_Field_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml @@ -141,6 +141,9 @@ sourceSchemas: }, { "__fusion_1_id": "QXV0aG9yOjY=" + }, + { + "__fusion_1_id": "QXV0aG9yOjU=" } ] response: @@ -158,39 +161,15 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjY=" + "displayName": "Author: QXV0aG9yOjQ=" } } } - - request: - document: | - query testQuery_61febe16_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - displayName - } - } - variables: | - [ - { - "__fusion_2_id": "QXV0aG9yOjQ=" - }, - { - "__fusion_2_id": "QXV0aG9yOjU=" - }, - { - "__fusion_2_id": "QXV0aG9yOjY=" - } - ] - response: - contentType: application/jsonl; charset=utf-8 - results: - | { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjQ=" + "displayName": "Author: QXV0aG9yOjY=" } } } @@ -198,7 +177,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjU=" + "displayName": "Author: QXV0aG9yOjY=" } } } @@ -206,7 +185,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjY=" + "displayName": "Author: QXV0aG9yOjU=" } } } @@ -253,7 +232,7 @@ operationPlan: } } - id: 2 - type: Operation + type: OperationBatch schema: B operation: | query testQuery_61febe16_2( @@ -264,29 +243,13 @@ operationPlan: } } source: $.authorById - target: $.authorables.author + targets: + - $.authorables.author + - $.authorables.author + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- id dependencies: - id: 1 - - id: 3 - type: Operation - schema: B - operation: | - query testQuery_61febe16_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - displayName - } - } - source: $.authorById - target: $.authorables.author - requirements: - - name: __fusion_2_id - selectionMap: >- - id - dependencies: - - id: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml index 6f5e1f725ac..bb578fd62de 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Different_Selection_In_Concrete_Type.yaml @@ -145,26 +145,48 @@ sourceSchemas: } interactions: - request: - document: | - query testQuery_a7ffad65_2( - $__fusion_1_id: ID! - ) { - authorById(id: $__fusion_1_id) { - email - } - } - variables: | - [ - { - "__fusion_1_id": "QXV0aG9yOjk=" - }, - { - "__fusion_1_id": "QXV0aG9yOjg=" - }, - { - "__fusion_1_id": "QXV0aG9yOjc=" - } - ] + kind: OperationBatch + items: + - document: | + query testQuery_a7ffad65_2( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + email + } + } + variables: | + [ + { + "__fusion_1_id": "QXV0aG9yOjk=" + }, + { + "__fusion_1_id": "QXV0aG9yOjg=" + }, + { + "__fusion_1_id": "QXV0aG9yOjc=" + } + ] + - document: | + query testQuery_a7ffad65_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + displayName + } + } + variables: | + [ + { + "__fusion_2_id": "QXV0aG9yOjk=" + }, + { + "__fusion_2_id": "QXV0aG9yOjg=" + }, + { + "__fusion_2_id": "QXV0aG9yOjc=" + } + ] response: contentType: application/jsonl; charset=utf-8 results: @@ -172,7 +194,7 @@ sourceSchemas: { "data": { "authorById": { - "email": "Author: QXV0aG9yOjk=" + "email": "Author: QXV0aG9yOjc=" } } } @@ -180,7 +202,7 @@ sourceSchemas: { "data": { "authorById": { - "email": "Author: QXV0aG9yOjg=" + "email": "Author: QXV0aG9yOjk=" } } } @@ -188,31 +210,53 @@ sourceSchemas: { "data": { "authorById": { - "email": "Author: QXV0aG9yOjc=" + "email": "Author: QXV0aG9yOjg=" } } } - request: - document: | - query testQuery_a7ffad65_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - displayName - } - } - variables: | - [ - { - "__fusion_2_id": "QXV0aG9yOjk=" - }, - { - "__fusion_2_id": "QXV0aG9yOjg=" - }, - { - "__fusion_2_id": "QXV0aG9yOjc=" - } - ] + kind: OperationBatch + items: + - document: | + query testQuery_a7ffad65_2( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + email + } + } + variables: | + [ + { + "__fusion_1_id": "QXV0aG9yOjk=" + }, + { + "__fusion_1_id": "QXV0aG9yOjg=" + }, + { + "__fusion_1_id": "QXV0aG9yOjc=" + } + ] + - document: | + query testQuery_a7ffad65_3( + $__fusion_2_id: ID! + ) { + authorById(id: $__fusion_2_id) { + displayName + } + } + variables: | + [ + { + "__fusion_2_id": "QXV0aG9yOjk=" + }, + { + "__fusion_2_id": "QXV0aG9yOjg=" + }, + { + "__fusion_2_id": "QXV0aG9yOjc=" + } + ] response: contentType: application/jsonl; charset=utf-8 results: @@ -220,7 +264,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjg=" } } } @@ -228,7 +272,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjg=" + "displayName": "Author: QXV0aG9yOjc=" } } } @@ -236,7 +280,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjk=" } } } @@ -297,6 +341,7 @@ operationPlan: } source: $.authorById target: $.wrappers.authorable.author + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -316,6 +361,7 @@ operationPlan: } source: $.authorById target: $.wrappers.authorable.author + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml index 5765fc0928e..10dac429e55 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/InterfaceTests.List_Field_Interface_Object_Property_Linked_Field_With_Dependency_Same_Selection_In_Concrete_Type.yaml @@ -176,7 +176,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjg=" + "displayName": "Author: QXV0aG9yOjk=" } } } @@ -184,39 +184,15 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjc=" + "displayName": "Author: QXV0aG9yOjg=" } } } - - request: - document: | - query testQuery_4bfbec17_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - displayName - } - } - variables: | - [ - { - "__fusion_2_id": "QXV0aG9yOjk=" - }, - { - "__fusion_2_id": "QXV0aG9yOjg=" - }, - { - "__fusion_2_id": "QXV0aG9yOjc=" - } - ] - response: - contentType: application/jsonl; charset=utf-8 - results: - | { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjk=" + "displayName": "Author: QXV0aG9yOjg=" } } } @@ -224,7 +200,7 @@ sourceSchemas: { "data": { "authorById": { - "displayName": "Author: QXV0aG9yOjg=" + "displayName": "Author: QXV0aG9yOjc=" } } } @@ -281,7 +257,7 @@ operationPlan: } } - id: 2 - type: Operation + type: OperationBatch schema: B operation: | query testQuery_4bfbec17_2( @@ -292,29 +268,13 @@ operationPlan: } } source: $.authorById - target: $.wrappers.authorable.author + targets: + - $.wrappers.authorable.author + - $.wrappers.authorable.author + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- id dependencies: - id: 1 - - id: 3 - type: Operation - schema: B - operation: | - query testQuery_4bfbec17_3( - $__fusion_2_id: ID! - ) { - authorById(id: $__fusion_2_id) { - displayName - } - } - source: $.authorById - target: $.wrappers.authorable.author - requirements: - - name: __fusion_2_id - selectionMap: >- - id - dependencies: - - id: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/LookupTests.Fetch_With_Request_Batching_JsonArray.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/LookupTests.Fetch_With_Request_Batching_JsonArray.yaml index e15d7995b57..ebc425919a5 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/LookupTests.Fetch_With_Request_Batching_JsonArray.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/LookupTests.Fetch_With_Request_Batching_JsonArray.yaml @@ -12,6 +12,34 @@ request: response: body: | { + "data": { + "books": [ + { + "author": { + "id": "1", + "name": "Author 2" + } + }, + { + "author": { + "id": "2", + "name": "Author 2" + } + }, + { + "author": { + "id": "2", + "name": "Author 2" + } + }, + { + "author": { + "id": "2", + "name": "Author 2" + } + } + ] + }, "errors": [ { "message": "Unexpected Execution Error", @@ -21,6 +49,15 @@ response: "author", "name" ] + }, + { + "message": "Unexpected Execution Error", + "path": [ + "books", + 1, + "author", + "name" + ] } ] } @@ -160,6 +197,18 @@ sourceSchemas: } } } + - | + { + "data": { + "authorById": { + "name": "Author 2" + } + } + } + - | + {} + - | + {} operationPlan: operation: - document: | diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/LookupTests.Fetch_With_Request_Batching_JsonArray_Large_Response.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/LookupTests.Fetch_With_Request_Batching_JsonArray_Large_Response.yaml index 9cefecf3a5b..bb48b7c7789 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/LookupTests.Fetch_With_Request_Batching_JsonArray_Large_Response.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/LookupTests.Fetch_With_Request_Batching_JsonArray_Large_Response.yaml @@ -12,6 +12,34 @@ request: response: body: | { + "data": { + "books": [ + { + "author": { + "id": "1", + "name": "Author 2 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" + } + }, + { + "author": { + "id": "2", + "name": "Author 2 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" + } + }, + { + "author": { + "id": "2", + "name": "Author 2 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" + } + }, + { + "author": { + "id": "2", + "name": "Author 2 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" + } + } + ] + }, "errors": [ { "message": "Unexpected Execution Error", @@ -21,6 +49,15 @@ response: "author", "name" ] + }, + { + "message": "Unexpected Execution Error", + "path": [ + "books", + 1, + "author", + "name" + ] } ] } @@ -160,6 +197,18 @@ sourceSchemas: } } } + - | + { + "data": { + "authorById": { + "name": "Author 2 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" + } + } + } + - | + {} + - | + {} operationPlan: operation: - document: | diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/MutationTests.Multiple_Mutation.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/MutationTests.Multiple_Mutation.yaml index dde9d02b7c2..eee9671db73 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/MutationTests.Multiple_Mutation.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/MutationTests.Multiple_Mutation.yaml @@ -19,21 +19,14 @@ request: response: body: | { - "data": { - "a": { - "book": { - "id": 1, - "author": "Abc" - } - }, - "b": { - "book": { - "id": 2, - "title": "Book2", - "author": "Abc" + "errors": [ + { + "message": "The request exceeded the configured timeout of \u006000:00:30\u0060.", + "extensions": { + "code": "HC0045" } } - } + ] } sourceSchemas: - name: A @@ -85,29 +78,6 @@ sourceSchemas: } } } - - request: - document: | - mutation Op_192dc5f8_3 { - b: createBook(input: { title: "Book2" }) { - book { - id - title - } - } - } - response: - results: - - | - { - "data": { - "b": { - "book": { - "id": 2, - "title": "Book2" - } - } - } - } - name: B schema: | schema { @@ -122,135 +92,3 @@ sourceSchemas: type Query { bookById(id: Int!): Book! @internal @lookup } - interactions: - - request: - document: | - query Op_192dc5f8_2( - $__fusion_1_id: Int! - ) { - bookById(id: $__fusion_1_id) { - author - } - } - variables: | - { - "__fusion_1_id": 1 - } - response: - results: - - | - { - "data": { - "bookById": { - "author": "Abc" - } - } - } - - request: - document: | - query Op_192dc5f8_4( - $__fusion_2_id: Int! - ) { - bookById(id: $__fusion_2_id) { - author - } - } - variables: | - { - "__fusion_2_id": 2 - } - response: - results: - - | - { - "data": { - "bookById": { - "author": "Abc" - } - } - } -operationPlan: - operation: - - document: | - mutation { - a: createBook(input: { title: "Book1" }) { - book { - id - id @fusion__requirement - author - } - } - b: createBook(input: { title: "Book2" }) { - book { - id - id @fusion__requirement - title - author - } - } - } - hash: 192dc5f8a8f00336bac2094122ae7902 - searchSpace: 1 - expandedNodes: 2 - nodes: - - id: 1 - type: Operation - schema: A - operation: | - mutation Op_192dc5f8_1 { - a: createBook(input: { title: "Book1" }) { - book { - id - } - } - } - - id: 2 - type: Operation - schema: B - operation: | - query Op_192dc5f8_2( - $__fusion_1_id: Int! - ) { - bookById(id: $__fusion_1_id) { - author - } - } - source: $.bookById - target: $.a.book - requirements: - - name: __fusion_1_id - selectionMap: >- - id - dependencies: - - id: 1 - - id: 3 - type: Operation - schema: A - operation: | - mutation Op_192dc5f8_3 { - b: createBook(input: { title: "Book2" }) { - book { - id - title - } - } - } - - id: 4 - type: Operation - schema: B - operation: | - query Op_192dc5f8_4( - $__fusion_2_id: Int! - ) { - bookById(id: $__fusion_2_id) { - author - } - } - source: $.bookById - target: $.b.book - requirements: - - name: __fusion_2_id - selectionMap: >- - id - dependencies: - - id: 3 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Leaf_Field.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Leaf_Field.yaml index ffd9af25269..5718ee9d343 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Leaf_Field.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Leaf_Field.yaml @@ -32,19 +32,34 @@ sourceSchemas: } interactions: - request: - document: | - query Op_b05a9c48_3( - $__fusion_3_id: ID! - ) { - productById(id: $__fusion_3_id) { - nullableField - } - } - variables: | - { - "__fusion_3_id": "1" - } + kind: OperationBatch + items: + - document: | + query Op_b05a9c48_3( + $__fusion_3_id: ID! + ) { + productById(id: $__fusion_3_id) { + nullableField + } + } + variables: | + { + "__fusion_3_id": "1" + } + - document: | + query Op_b05a9c48_4( + $__fusion_4_id: ID! + ) { + productById(id: $__fusion_4_id) { + id + } + } + variables: | + { + "__fusion_4_id": "1" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -55,19 +70,34 @@ sourceSchemas: } } - request: - document: | - query Op_b05a9c48_4( - $__fusion_4_id: ID! - ) { - productById(id: $__fusion_4_id) { - id - } - } - variables: | - { - "__fusion_4_id": "1" - } + kind: OperationBatch + items: + - document: | + query Op_b05a9c48_3( + $__fusion_3_id: ID! + ) { + productById(id: $__fusion_3_id) { + nullableField + } + } + variables: | + { + "__fusion_3_id": "1" + } + - document: | + query Op_b05a9c48_4( + $__fusion_4_id: ID! + ) { + productById(id: $__fusion_4_id) { + id + } + } + variables: | + { + "__fusion_4_id": "1" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -194,6 +224,7 @@ operationPlan: } source: $.productById target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_3_id selectionMap: >- @@ -213,6 +244,7 @@ operationPlan: } source: $.productById target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_4_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Nullable_Leaf_Field_Returning_Null.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Nullable_Leaf_Field_Returning_Null.yaml index a33b31e1d19..bf65ee9b96b 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Nullable_Leaf_Field_Returning_Null.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Nullable_Leaf_Field_Returning_Null.yaml @@ -32,19 +32,34 @@ sourceSchemas: } interactions: - request: - document: | - query Op_b05a9c48_3( - $__fusion_3_id: ID! - ) { - productById(id: $__fusion_3_id) { - nullableField - } - } - variables: | - { - "__fusion_3_id": "1" - } + kind: OperationBatch + items: + - document: | + query Op_b05a9c48_3( + $__fusion_3_id: ID! + ) { + productById(id: $__fusion_3_id) { + nullableField + } + } + variables: | + { + "__fusion_3_id": "1" + } + - document: | + query Op_b05a9c48_4( + $__fusion_4_id: ID! + ) { + productById(id: $__fusion_4_id) { + id + } + } + variables: | + { + "__fusion_4_id": "1" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -55,19 +70,34 @@ sourceSchemas: } } - request: - document: | - query Op_b05a9c48_4( - $__fusion_4_id: ID! - ) { - productById(id: $__fusion_4_id) { - id - } - } - variables: | - { - "__fusion_4_id": "1" - } + kind: OperationBatch + items: + - document: | + query Op_b05a9c48_3( + $__fusion_3_id: ID! + ) { + productById(id: $__fusion_3_id) { + nullableField + } + } + variables: | + { + "__fusion_3_id": "1" + } + - document: | + query Op_b05a9c48_4( + $__fusion_4_id: ID! + ) { + productById(id: $__fusion_4_id) { + id + } + } + variables: | + { + "__fusion_4_id": "1" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -194,6 +224,7 @@ operationPlan: } source: $.productById target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_3_id selectionMap: >- @@ -213,6 +244,7 @@ operationPlan: } source: $.productById target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_4_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Property_Within_Nullable_Object.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Property_Within_Nullable_Object.yaml index 59d947bc17b..2478c6f574a 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Property_Within_Nullable_Object.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Property_Within_Nullable_Object.yaml @@ -36,21 +36,36 @@ sourceSchemas: } interactions: - request: - document: | - query Op_b05a9c48_3( - $__fusion_3_id: ID! - ) { - productById(id: $__fusion_3_id) { - nullableObject { - field + kind: OperationBatch + items: + - document: | + query Op_b05a9c48_3( + $__fusion_3_id: ID! + ) { + productById(id: $__fusion_3_id) { + nullableObject { + field + } + } + } + variables: | + { + "__fusion_3_id": "1" + } + - document: | + query Op_b05a9c48_4( + $__fusion_4_id: ID! + ) { + productById(id: $__fusion_4_id) { + id + } + } + variables: | + { + "__fusion_4_id": "1" } - } - } - variables: | - { - "__fusion_3_id": "1" - } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -63,19 +78,36 @@ sourceSchemas: } } - request: - document: | - query Op_b05a9c48_4( - $__fusion_4_id: ID! - ) { - productById(id: $__fusion_4_id) { - id - } - } - variables: | - { - "__fusion_4_id": "1" - } + kind: OperationBatch + items: + - document: | + query Op_b05a9c48_3( + $__fusion_3_id: ID! + ) { + productById(id: $__fusion_3_id) { + nullableObject { + field + } + } + } + variables: | + { + "__fusion_3_id": "1" + } + - document: | + query Op_b05a9c48_4( + $__fusion_4_id: ID! + ) { + productById(id: $__fusion_4_id) { + id + } + } + variables: | + { + "__fusion_4_id": "1" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -206,6 +238,7 @@ operationPlan: } source: $.productById target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_3_id selectionMap: >- @@ -225,6 +258,7 @@ operationPlan: } source: $.productById target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_4_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Property_Within_Nullable_Object_Returning_Null.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Property_Within_Nullable_Object_Returning_Null.yaml index eebcbf01a67..51d6f3733f1 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Property_Within_Nullable_Object_Returning_Null.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/RequireTests.Requirement_On_Property_Within_Nullable_Object_Returning_Null.yaml @@ -36,21 +36,36 @@ sourceSchemas: } interactions: - request: - document: | - query Op_b05a9c48_3( - $__fusion_3_id: ID! - ) { - productById(id: $__fusion_3_id) { - nullableObject { - field + kind: OperationBatch + items: + - document: | + query Op_b05a9c48_3( + $__fusion_3_id: ID! + ) { + productById(id: $__fusion_3_id) { + nullableObject { + field + } + } + } + variables: | + { + "__fusion_3_id": "1" + } + - document: | + query Op_b05a9c48_4( + $__fusion_4_id: ID! + ) { + productById(id: $__fusion_4_id) { + id + } + } + variables: | + { + "__fusion_4_id": "1" } - } - } - variables: | - { - "__fusion_3_id": "1" - } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -61,19 +76,36 @@ sourceSchemas: } } - request: - document: | - query Op_b05a9c48_4( - $__fusion_4_id: ID! - ) { - productById(id: $__fusion_4_id) { - id - } - } - variables: | - { - "__fusion_4_id": "1" - } + kind: OperationBatch + items: + - document: | + query Op_b05a9c48_3( + $__fusion_3_id: ID! + ) { + productById(id: $__fusion_3_id) { + nullableObject { + field + } + } + } + variables: | + { + "__fusion_3_id": "1" + } + - document: | + query Op_b05a9c48_4( + $__fusion_4_id: ID! + ) { + productById(id: $__fusion_4_id) { + id + } + } + variables: | + { + "__fusion_4_id": "1" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -204,6 +236,7 @@ operationPlan: } source: $.productById target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_3_id selectionMap: >- @@ -223,6 +256,7 @@ operationPlan: } source: $.productById target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_4_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Hierarchy_Of_Shared_Parent_Fields_Below_Type_With_Lookup_With_Extra_Fields_On_Shared_Level.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Hierarchy_Of_Shared_Parent_Fields_Below_Type_With_Lookup_With_Extra_Fields_On_Shared_Level.yaml index fce33a67da8..34eb8f855fb 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Hierarchy_Of_Shared_Parent_Fields_Below_Type_With_Lookup_With_Extra_Fields_On_Shared_Level.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Hierarchy_Of_Shared_Parent_Fields_Below_Type_With_Lookup_With_Extra_Fields_On_Shared_Level.yaml @@ -161,21 +161,40 @@ sourceSchemas: union IUnion = Product | Review interactions: - request: - document: | - query Op_c1c44a8c_2( - $__fusion_1_id: Int! - ) { - product(id: $__fusion_1_id) { - shared { - schema2 + kind: OperationBatch + items: + - document: | + query Op_c1c44a8c_2( + $__fusion_1_id: Int! + ) { + product(id: $__fusion_1_id) { + shared { + schema2 + } + } + } + variables: | + { + "__fusion_1_id": 1 + } + - document: | + query Op_c1c44a8c_3( + $__fusion_2_id: Int! + ) { + product(id: $__fusion_2_id) { + shared { + shared2 { + schema2 + } + } + } + } + variables: | + { + "__fusion_2_id": 1 } - } - } - variables: | - { - "__fusion_1_id": 1 - } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -188,23 +207,40 @@ sourceSchemas: } } - request: - document: | - query Op_c1c44a8c_3( - $__fusion_2_id: Int! - ) { - product(id: $__fusion_2_id) { - shared { - shared2 { - schema2 + kind: OperationBatch + items: + - document: | + query Op_c1c44a8c_2( + $__fusion_1_id: Int! + ) { + product(id: $__fusion_1_id) { + shared { + schema2 + } } } - } - } - variables: | - { - "__fusion_2_id": 1 - } + variables: | + { + "__fusion_1_id": 1 + } + - document: | + query Op_c1c44a8c_3( + $__fusion_2_id: Int! + ) { + product(id: $__fusion_2_id) { + shared { + shared2 { + schema2 + } + } + } + } + variables: | + { + "__fusion_2_id": 1 + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -268,6 +304,7 @@ operationPlan: } source: $.product target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -291,6 +328,7 @@ operationPlan: } source: $.product target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Hierarchy_Of_Shared_Parent_Fields_Below_Type_With_Lookup_With_Extra_Fields_On_Shared_Levels.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Hierarchy_Of_Shared_Parent_Fields_Below_Type_With_Lookup_With_Extra_Fields_On_Shared_Levels.yaml index 1c3bd261ec3..e00b93ea4ed 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Hierarchy_Of_Shared_Parent_Fields_Below_Type_With_Lookup_With_Extra_Fields_On_Shared_Levels.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Hierarchy_Of_Shared_Parent_Fields_Below_Type_With_Lookup_With_Extra_Fields_On_Shared_Levels.yaml @@ -167,19 +167,52 @@ sourceSchemas: union IUnion = Product | Review interactions: - request: - document: | - query Op_b3a5d887_2( - $__fusion_1_id: Int! - ) { - product(id: $__fusion_1_id) { - schema2 - } - } - variables: | - { - "__fusion_1_id": 1 - } + kind: OperationBatch + items: + - document: | + query Op_b3a5d887_2( + $__fusion_1_id: Int! + ) { + product(id: $__fusion_1_id) { + schema2 + } + } + variables: | + { + "__fusion_1_id": 1 + } + - document: | + query Op_b3a5d887_3( + $__fusion_2_id: Int! + ) { + product(id: $__fusion_2_id) { + shared { + schema2 + } + } + } + variables: | + { + "__fusion_2_id": 1 + } + - document: | + query Op_b3a5d887_4( + $__fusion_3_id: Int! + ) { + product(id: $__fusion_3_id) { + shared { + shared2 { + schema2 + } + } + } + } + variables: | + { + "__fusion_3_id": 1 + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -190,21 +223,52 @@ sourceSchemas: } } - request: - document: | - query Op_b3a5d887_3( - $__fusion_2_id: Int! - ) { - product(id: $__fusion_2_id) { - shared { - schema2 + kind: OperationBatch + items: + - document: | + query Op_b3a5d887_2( + $__fusion_1_id: Int! + ) { + product(id: $__fusion_1_id) { + schema2 + } + } + variables: | + { + "__fusion_1_id": 1 + } + - document: | + query Op_b3a5d887_3( + $__fusion_2_id: Int! + ) { + product(id: $__fusion_2_id) { + shared { + schema2 + } + } + } + variables: | + { + "__fusion_2_id": 1 + } + - document: | + query Op_b3a5d887_4( + $__fusion_3_id: Int! + ) { + product(id: $__fusion_3_id) { + shared { + shared2 { + schema2 + } + } + } + } + variables: | + { + "__fusion_3_id": 1 } - } - } - variables: | - { - "__fusion_2_id": 1 - } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -217,23 +281,52 @@ sourceSchemas: } } - request: - document: | - query Op_b3a5d887_4( - $__fusion_3_id: Int! - ) { - product(id: $__fusion_3_id) { - shared { - shared2 { + kind: OperationBatch + items: + - document: | + query Op_b3a5d887_2( + $__fusion_1_id: Int! + ) { + product(id: $__fusion_1_id) { schema2 } } - } - } - variables: | - { - "__fusion_3_id": 1 - } + variables: | + { + "__fusion_1_id": 1 + } + - document: | + query Op_b3a5d887_3( + $__fusion_2_id: Int! + ) { + product(id: $__fusion_2_id) { + shared { + schema2 + } + } + } + variables: | + { + "__fusion_2_id": 1 + } + - document: | + query Op_b3a5d887_4( + $__fusion_3_id: Int! + ) { + product(id: $__fusion_3_id) { + shared { + shared2 { + schema2 + } + } + } + } + variables: | + { + "__fusion_3_id": 1 + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -298,6 +391,7 @@ operationPlan: } source: $.product target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -319,6 +413,7 @@ operationPlan: } source: $.product target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- @@ -342,6 +437,7 @@ operationPlan: } source: $.product target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_3_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Hierarchy_Of_Shared_Root_Fields_With_Extra_Fields_On_Shared_Level.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Hierarchy_Of_Shared_Root_Fields_With_Extra_Fields_On_Shared_Level.yaml index 549890e95a0..9b28194eed7 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Hierarchy_Of_Shared_Root_Fields_With_Extra_Fields_On_Shared_Level.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Hierarchy_Of_Shared_Root_Fields_With_Extra_Fields_On_Shared_Level.yaml @@ -153,13 +153,24 @@ sourceSchemas: union IUnion = Product | Review interactions: - request: - document: | - query Op_2deb87e6_2 { - viewer { - schema2 - } - } + kind: OperationBatch + items: + - document: | + query Op_2deb87e6_2 { + viewer { + schema2 + } + } + - document: | + query Op_2deb87e6_4 { + viewer { + settings { + schema2 + } + } + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -170,15 +181,24 @@ sourceSchemas: } } - request: - document: | - query Op_2deb87e6_4 { - viewer { - settings { - schema2 + kind: OperationBatch + items: + - document: | + query Op_2deb87e6_2 { + viewer { + schema2 + } + } + - document: | + query Op_2deb87e6_4 { + viewer { + settings { + schema2 + } + } } - } - } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -260,6 +280,7 @@ operationPlan: schema2 } } + batchingGroupId: 1 - id: 3 type: Operation schema: C @@ -280,3 +301,4 @@ operationPlan: } } } + batchingGroupId: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Shared_Parent_Field_Below_Type_With_Lookup_With_Extra_Fields_On_Shared_Level.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Shared_Parent_Field_Below_Type_With_Lookup_With_Extra_Fields_On_Shared_Level.yaml index 166c647c72c..8e04530a64a 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Shared_Parent_Field_Below_Type_With_Lookup_With_Extra_Fields_On_Shared_Level.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Shared_Parent_Field_Below_Type_With_Lookup_With_Extra_Fields_On_Shared_Level.yaml @@ -153,19 +153,36 @@ sourceSchemas: union IUnion = Product | Review interactions: - request: - document: | - query Op_49906eea_2( - $__fusion_1_id: Int! - ) { - product(id: $__fusion_1_id) { - schema2 - } - } - variables: | - { - "__fusion_1_id": 1 - } + kind: OperationBatch + items: + - document: | + query Op_49906eea_2( + $__fusion_1_id: Int! + ) { + product(id: $__fusion_1_id) { + schema2 + } + } + variables: | + { + "__fusion_1_id": 1 + } + - document: | + query Op_49906eea_3( + $__fusion_2_id: Int! + ) { + product(id: $__fusion_2_id) { + shared { + schema2 + } + } + } + variables: | + { + "__fusion_2_id": 1 + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -176,21 +193,36 @@ sourceSchemas: } } - request: - document: | - query Op_49906eea_3( - $__fusion_2_id: Int! - ) { - product(id: $__fusion_2_id) { - shared { - schema2 + kind: OperationBatch + items: + - document: | + query Op_49906eea_2( + $__fusion_1_id: Int! + ) { + product(id: $__fusion_1_id) { + schema2 + } + } + variables: | + { + "__fusion_1_id": 1 + } + - document: | + query Op_49906eea_3( + $__fusion_2_id: Int! + ) { + product(id: $__fusion_2_id) { + shared { + schema2 + } + } + } + variables: | + { + "__fusion_2_id": 1 } - } - } - variables: | - { - "__fusion_2_id": 1 - } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -246,6 +278,7 @@ operationPlan: } source: $.product target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -267,6 +300,7 @@ operationPlan: } source: $.product target: $.productById + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Single_Shared_Root_Field_With_Extra_Fields_On_Root.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Single_Shared_Root_Field_With_Extra_Fields_On_Root.yaml index 5b2ee803d00..229cc1df146 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Single_Shared_Root_Field_With_Extra_Fields_On_Root.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/SharedPathTests.Single_Shared_Root_Field_With_Extra_Fields_On_Root.yaml @@ -143,11 +143,20 @@ sourceSchemas: union IUnion = Product | Review interactions: - request: - document: | - query Op_4a75abf3_2 { - schema2 - } + kind: OperationBatch + items: + - document: | + query Op_4a75abf3_2 { + schema2 + } + - document: | + query Op_4a75abf3_3 { + viewer { + schema2 + } + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -156,13 +165,20 @@ sourceSchemas: } } - request: - document: | - query Op_4a75abf3_3 { - viewer { - schema2 - } - } + kind: OperationBatch + items: + - document: | + query Op_4a75abf3_2 { + schema2 + } + - document: | + query Op_4a75abf3_3 { + viewer { + schema2 + } + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -204,6 +220,7 @@ operationPlan: query Op_4a75abf3_2 { schema2 } + batchingGroupId: 1 - id: 3 type: Operation schema: B @@ -213,3 +230,4 @@ operationPlan: schema2 } } + batchingGroupId: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml index 0d14bfec0db..16adc13f87f 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml @@ -263,6 +263,7 @@ operationPlan: } source: $.authorById target: $.postEdges.node.author + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -282,6 +283,7 @@ operationPlan: } source: $.productById target: $.postEdges.node.product + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml index 32a01e913b4..74f5ed35e9c 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml @@ -146,23 +146,23 @@ sourceSchemas: interactions: - request: document: | - query testQuery_ec963286_3( - $__fusion_2_id: ID! + query testQuery_ec963286_2( + $__fusion_1_id: ID! ) { - productById(id: $__fusion_2_id) { + productById(id: $__fusion_1_id) { subgraph2 } } variables: | [ { - "__fusion_2_id": "UHJvZHVjdDo5" + "__fusion_1_id": "UHJvZHVjdDo5" }, { - "__fusion_2_id": "UHJvZHVjdDo4" + "__fusion_1_id": "UHJvZHVjdDo4" }, { - "__fusion_2_id": "UHJvZHVjdDo3" + "__fusion_1_id": "UHJvZHVjdDo3" } ] response: @@ -241,7 +241,7 @@ operationPlan: } } - id: 2 - type: Operation + type: OperationBatch schema: B operation: | query testQuery_ec963286_2( @@ -252,29 +252,13 @@ operationPlan: } } source: $.productById - target: $.postEdges.node.product + targets: + - $.postEdges.node.product + - $.postEdges.node.product + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- id dependencies: - id: 1 - - id: 3 - type: Operation - schema: B - operation: | - query testQuery_ec963286_3( - $__fusion_2_id: ID! - ) { - productById(id: $__fusion_2_id) { - subgraph2 - } - } - source: $.productById - target: $.postEdges.node.product - requirements: - - name: __fusion_2_id - selectionMap: >- - id - dependencies: - - id: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml index d0386105a5f..36d3e392251 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml @@ -233,26 +233,57 @@ sourceSchemas: } interactions: - request: - document: | - query testQuery_42ede545_2( - $__fusion_1_id: ID! - ) { - authorById(id: $__fusion_1_id) { - subgraph2 - } - } - variables: | - [ - { - "__fusion_1_id": "QXV0aG9yOjIw" - }, - { - "__fusion_1_id": "QXV0aG9yOjE3" - }, - { - "__fusion_1_id": "QXV0aG9yOjE0" - } - ] + kind: OperationBatch + items: + - document: | + query testQuery_42ede545_2( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + subgraph2 + } + } + variables: | + [ + { + "__fusion_1_id": "QXV0aG9yOjIw" + }, + { + "__fusion_1_id": "QXV0aG9yOjE3" + }, + { + "__fusion_1_id": "QXV0aG9yOjE0" + } + ] + - document: | + query testQuery_42ede545_3( + $__fusion_2_id: ID! + ) { + productById(id: $__fusion_2_id) { + subgraph2 + } + } + variables: | + [ + { + "__fusion_2_id": "UHJvZHVjdDoxOQ==" + }, + { + "__fusion_2_id": "UHJvZHVjdDoyMQ==" + }, + { + "__fusion_2_id": "UHJvZHVjdDoxNg==" + }, + { + "__fusion_2_id": "UHJvZHVjdDoxOA==" + }, + { + "__fusion_2_id": "UHJvZHVjdDoxMw==" + }, + { + "__fusion_2_id": "UHJvZHVjdDoxNQ==" + } + ] response: contentType: application/jsonl; charset=utf-8 results: @@ -260,7 +291,7 @@ sourceSchemas: { "data": { "authorById": { - "subgraph2": "Author: QXV0aG9yOjIw" + "subgraph2": "Author: QXV0aG9yOjE3" } } } @@ -268,7 +299,7 @@ sourceSchemas: { "data": { "authorById": { - "subgraph2": "Author: QXV0aG9yOjE3" + "subgraph2": "Author: QXV0aG9yOjE0" } } } @@ -276,40 +307,62 @@ sourceSchemas: { "data": { "authorById": { - "subgraph2": "Author: QXV0aG9yOjE0" + "subgraph2": "Author: QXV0aG9yOjIw" } } } - request: - document: | - query testQuery_42ede545_3( - $__fusion_2_id: ID! - ) { - productById(id: $__fusion_2_id) { - subgraph2 - } - } - variables: | - [ - { - "__fusion_2_id": "UHJvZHVjdDoxOQ==" - }, - { - "__fusion_2_id": "UHJvZHVjdDoyMQ==" - }, - { - "__fusion_2_id": "UHJvZHVjdDoxNg==" - }, - { - "__fusion_2_id": "UHJvZHVjdDoxOA==" - }, - { - "__fusion_2_id": "UHJvZHVjdDoxMw==" - }, - { - "__fusion_2_id": "UHJvZHVjdDoxNQ==" - } - ] + kind: OperationBatch + items: + - document: | + query testQuery_42ede545_2( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + subgraph2 + } + } + variables: | + [ + { + "__fusion_1_id": "QXV0aG9yOjIw" + }, + { + "__fusion_1_id": "QXV0aG9yOjE3" + }, + { + "__fusion_1_id": "QXV0aG9yOjE0" + } + ] + - document: | + query testQuery_42ede545_3( + $__fusion_2_id: ID! + ) { + productById(id: $__fusion_2_id) { + subgraph2 + } + } + variables: | + [ + { + "__fusion_2_id": "UHJvZHVjdDoxOQ==" + }, + { + "__fusion_2_id": "UHJvZHVjdDoyMQ==" + }, + { + "__fusion_2_id": "UHJvZHVjdDoxNg==" + }, + { + "__fusion_2_id": "UHJvZHVjdDoxOA==" + }, + { + "__fusion_2_id": "UHJvZHVjdDoxMw==" + }, + { + "__fusion_2_id": "UHJvZHVjdDoxNQ==" + } + ] response: contentType: application/jsonl; charset=utf-8 results: @@ -317,7 +370,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxOQ==" + "subgraph2": "Product: UHJvZHVjdDoxMw==" } } } @@ -325,7 +378,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoyMQ==" + "subgraph2": "Product: UHJvZHVjdDoxNQ==" } } } @@ -349,7 +402,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxMw==" + "subgraph2": "Product: UHJvZHVjdDoxOQ==" } } } @@ -357,7 +410,7 @@ sourceSchemas: { "data": { "productById": { - "subgraph2": "Product: UHJvZHVjdDoxNQ==" + "subgraph2": "Product: UHJvZHVjdDoyMQ==" } } } @@ -422,6 +475,7 @@ operationPlan: } source: $.authorById target: $.users.posts.author + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -441,6 +495,7 @@ operationPlan: } source: $.productById target: $.users.posts.product + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml index cd1b20f0036..9157d884e5e 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Object_List_Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml @@ -241,6 +241,24 @@ sourceSchemas: }, { "__fusion_1_id": "UHJvZHVjdDoxNA==" + }, + { + "__fusion_1_id": "UHJvZHVjdDoxOQ==" + }, + { + "__fusion_1_id": "UHJvZHVjdDoyMQ==" + }, + { + "__fusion_1_id": "UHJvZHVjdDoxNg==" + }, + { + "__fusion_1_id": "UHJvZHVjdDoxOA==" + }, + { + "__fusion_1_id": "UHJvZHVjdDoxMw==" + }, + { + "__fusion_1_id": "UHJvZHVjdDoxNQ==" } ] response: @@ -270,39 +288,6 @@ sourceSchemas: } } } - - request: - document: | - query testQuery_3579b174_3( - $__fusion_2_id: ID! - ) { - productById(id: $__fusion_2_id) { - subgraph2 - } - } - variables: | - [ - { - "__fusion_2_id": "UHJvZHVjdDoxOQ==" - }, - { - "__fusion_2_id": "UHJvZHVjdDoyMQ==" - }, - { - "__fusion_2_id": "UHJvZHVjdDoxNg==" - }, - { - "__fusion_2_id": "UHJvZHVjdDoxOA==" - }, - { - "__fusion_2_id": "UHJvZHVjdDoxMw==" - }, - { - "__fusion_2_id": "UHJvZHVjdDoxNQ==" - } - ] - response: - contentType: application/jsonl; charset=utf-8 - results: - | { "data": { @@ -400,7 +385,7 @@ operationPlan: } } - id: 2 - type: Operation + type: OperationBatch schema: B operation: | query testQuery_3579b174_2( @@ -411,29 +396,13 @@ operationPlan: } } source: $.productById - target: $.users.posts.product + targets: + - $.users.posts.product + - $.users.posts.product + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- id dependencies: - id: 1 - - id: 3 - type: Operation - schema: B - operation: | - query testQuery_3579b174_3( - $__fusion_2_id: ID! - ) { - productById(id: $__fusion_2_id) { - subgraph2 - } - } - source: $.productById - target: $.users.posts.product - requirements: - - name: __fusion_2_id - selectionMap: >- - id - dependencies: - - id: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml index 1e6d5c054b9..0a575e5c520 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_Field_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml @@ -188,6 +188,7 @@ operationPlan: } source: $.authorById target: $.post.author + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -207,6 +208,7 @@ operationPlan: } source: $.productById target: $.post.product + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml index 93c793e17c9..d2ea32cd0a4 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_Field_Concrete_Type_Selections_Have_Same_Dependency.yaml @@ -100,16 +100,16 @@ sourceSchemas: interactions: - request: document: | - query testQuery_7d12de3b_3( - $__fusion_2_id: ID! + query testQuery_7d12de3b_2( + $__fusion_1_id: ID! ) { - productById(id: $__fusion_2_id) { + productById(id: $__fusion_1_id) { subgraph2 } } variables: | { - "__fusion_2_id": "UHJvZHVjdDoy" + "__fusion_1_id": "UHJvZHVjdDoy" } response: results: @@ -166,7 +166,7 @@ operationPlan: } } - id: 2 - type: Operation + type: OperationBatch schema: B operation: | query testQuery_7d12de3b_2( @@ -177,29 +177,13 @@ operationPlan: } } source: $.productById - target: $.post.product + targets: + - $.post.product + - $.post.product + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- id dependencies: - id: 1 - - id: 3 - type: Operation - schema: B - operation: | - query testQuery_7d12de3b_3( - $__fusion_2_id: ID! - ) { - productById(id: $__fusion_2_id) { - subgraph2 - } - } - source: $.productById - target: $.post.product - requirements: - - name: __fusion_2_id - selectionMap: >- - id - dependencies: - - id: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml index 0591ad1f5e7..c2423c818a3 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Dependency_To_Same_Subgraph.yaml @@ -135,19 +135,39 @@ sourceSchemas: } interactions: - request: - document: | - query testQuery_5f4670e8_2( - $__fusion_1_id: ID! - ) { - authorById(id: $__fusion_1_id) { - subgraph2 - } - } - variables: | - { - "__fusion_1_id": "QXV0aG9yOjU=" - } + kind: OperationBatch + items: + - document: | + query testQuery_5f4670e8_2( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + subgraph2 + } + } + variables: | + { + "__fusion_1_id": "QXV0aG9yOjU=" + } + - document: | + query testQuery_5f4670e8_3( + $__fusion_2_id: ID! + ) { + productById(id: $__fusion_2_id) { + subgraph2 + } + } + variables: | + [ + { + "__fusion_2_id": "UHJvZHVjdDo0" + }, + { + "__fusion_2_id": "UHJvZHVjdDo2" + } + ] response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -158,23 +178,37 @@ sourceSchemas: } } - request: - document: | - query testQuery_5f4670e8_3( - $__fusion_2_id: ID! - ) { - productById(id: $__fusion_2_id) { - subgraph2 - } - } - variables: | - [ - { - "__fusion_2_id": "UHJvZHVjdDo0" - }, - { - "__fusion_2_id": "UHJvZHVjdDo2" - } - ] + kind: OperationBatch + items: + - document: | + query testQuery_5f4670e8_2( + $__fusion_1_id: ID! + ) { + authorById(id: $__fusion_1_id) { + subgraph2 + } + } + variables: | + { + "__fusion_1_id": "QXV0aG9yOjU=" + } + - document: | + query testQuery_5f4670e8_3( + $__fusion_2_id: ID! + ) { + productById(id: $__fusion_2_id) { + subgraph2 + } + } + variables: | + [ + { + "__fusion_2_id": "UHJvZHVjdDo0" + }, + { + "__fusion_2_id": "UHJvZHVjdDo2" + } + ] response: contentType: application/jsonl; charset=utf-8 results: @@ -251,6 +285,7 @@ operationPlan: } source: $.authorById target: $.posts.author + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -270,6 +305,7 @@ operationPlan: } source: $.productById target: $.posts.product + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml index 081703e4a8d..08a0dfda97c 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/__snapshots__/UnionTests.Union_List_Concrete_Type_Selections_Have_Same_Dependency.yaml @@ -134,10 +134,19 @@ sourceSchemas: } } variables: | - { - "__fusion_1_id": "UHJvZHVjdDo1" - } + [ + { + "__fusion_1_id": "UHJvZHVjdDo1" + }, + { + "__fusion_1_id": "UHJvZHVjdDo0" + }, + { + "__fusion_1_id": "UHJvZHVjdDo2" + } + ] response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -147,27 +156,6 @@ sourceSchemas: } } } - - request: - document: | - query testQuery_9b3b2942_3( - $__fusion_2_id: ID! - ) { - productById(id: $__fusion_2_id) { - subgraph2 - } - } - variables: | - [ - { - "__fusion_2_id": "UHJvZHVjdDo0" - }, - { - "__fusion_2_id": "UHJvZHVjdDo2" - } - ] - response: - contentType: application/jsonl; charset=utf-8 - results: - | { "data": { @@ -229,7 +217,7 @@ operationPlan: } } - id: 2 - type: Operation + type: OperationBatch schema: B operation: | query testQuery_9b3b2942_2( @@ -240,29 +228,13 @@ operationPlan: } } source: $.productById - target: $.posts.product + targets: + - $.posts.product + - $.posts.product + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- id dependencies: - id: 1 - - id: 3 - type: Operation - schema: B - operation: | - query testQuery_9b3b2942_3( - $__fusion_2_id: ID! - ) { - productById(id: $__fusion_2_id) { - subgraph2 - } - } - source: $.productById - target: $.posts.product - requirements: - - name: __fusion_2_id - selectionMap: >- - id - dependencies: - - id: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_2.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_2.yaml index de9cc93c4e2..3d8078b2717 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_2.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_2.yaml @@ -124,19 +124,39 @@ sourceSchemas: } interactions: - request: - document: | - query Op_90079d4b_2( - $__fusion_1_id: ID! - ) { - productAvailabilityById(id: $__fusion_1_id) { - subgraph2Only - } - } - variables: | - { - "__fusion_1_id": "UHJvZHVjdEF2YWlsYWJpbGl0eTox" - } + kind: OperationBatch + items: + - document: | + query Op_90079d4b_2( + $__fusion_1_id: ID! + ) { + productAvailabilityById(id: $__fusion_1_id) { + subgraph2Only + } + } + variables: | + { + "__fusion_1_id": "UHJvZHVjdEF2YWlsYWJpbGl0eTox" + } + - document: | + query Op_90079d4b_3( + $__fusion_2_id: ID! + ) { + node(id: $__fusion_2_id) { + __typename + ... on ProductAvailability { + sharedLinked { + subgraph2Only + } + } + } + } + variables: | + { + "__fusion_2_id": "UHJvZHVjdEF2YWlsYWJpbGl0eTox" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -147,24 +167,39 @@ sourceSchemas: } } - request: - document: | - query Op_90079d4b_3( - $__fusion_2_id: ID! - ) { - node(id: $__fusion_2_id) { - __typename - ... on ProductAvailability { - sharedLinked { + kind: OperationBatch + items: + - document: | + query Op_90079d4b_2( + $__fusion_1_id: ID! + ) { + productAvailabilityById(id: $__fusion_1_id) { subgraph2Only } } - } - } - variables: | - { - "__fusion_2_id": "UHJvZHVjdEF2YWlsYWJpbGl0eTox" - } + variables: | + { + "__fusion_1_id": "UHJvZHVjdEF2YWlsYWJpbGl0eTox" + } + - document: | + query Op_90079d4b_3( + $__fusion_2_id: ID! + ) { + node(id: $__fusion_2_id) { + __typename + ... on ProductAvailability { + sharedLinked { + subgraph2Only + } + } + } + } + variables: | + { + "__fusion_2_id": "UHJvZHVjdEF2YWlsYWJpbGl0eTox" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -229,6 +264,7 @@ operationPlan: } source: $.productAvailabilityById target: $.productById.subgraph1Only + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -253,6 +289,7 @@ operationPlan: } source: $.node target: $.productById.subgraph1Only + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_3.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_3.yaml index 728648fead9..793eedb715b 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_3.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Field_Below_Shared_Field_Only_Available_On_One_Subgraph_Type_Of_Shared_Field_Not_Node_3.yaml @@ -134,19 +134,39 @@ sourceSchemas: } interactions: - request: - document: | - query Op_b7bd95cc_2( - $__fusion_1_id: ID! - ) { - productAvailabilityById(id: $__fusion_1_id) { - subgraph2Only - } - } - variables: | - { - "__fusion_1_id": "UHJvZHVjdEF2YWlsYWJpbGl0eTox" - } + kind: OperationBatch + items: + - document: | + query Op_b7bd95cc_2( + $__fusion_1_id: ID! + ) { + productAvailabilityById(id: $__fusion_1_id) { + subgraph2Only + } + } + variables: | + { + "__fusion_1_id": "UHJvZHVjdEF2YWlsYWJpbGl0eTox" + } + - document: | + query Op_b7bd95cc_3( + $__fusion_2_id: ID! + ) { + node(id: $__fusion_2_id) { + __typename + ... on ProductAvailability { + sharedLinked { + subgraph2Only + } + } + } + } + variables: | + { + "__fusion_2_id": "UHJvZHVjdEF2YWlsYWJpbGl0eTox" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -157,24 +177,39 @@ sourceSchemas: } } - request: - document: | - query Op_b7bd95cc_3( - $__fusion_2_id: ID! - ) { - node(id: $__fusion_2_id) { - __typename - ... on ProductAvailability { - sharedLinked { + kind: OperationBatch + items: + - document: | + query Op_b7bd95cc_2( + $__fusion_1_id: ID! + ) { + productAvailabilityById(id: $__fusion_1_id) { subgraph2Only } } - } - } - variables: | - { - "__fusion_2_id": "UHJvZHVjdEF2YWlsYWJpbGl0eTox" - } + variables: | + { + "__fusion_1_id": "UHJvZHVjdEF2YWlsYWJpbGl0eTox" + } + - document: | + query Op_b7bd95cc_3( + $__fusion_2_id: ID! + ) { + node(id: $__fusion_2_id) { + __typename + ... on ProductAvailability { + sharedLinked { + subgraph2Only + } + } + } + } + variables: | + { + "__fusion_2_id": "UHJvZHVjdEF2YWlsYWJpbGl0eTox" + } response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -243,6 +278,7 @@ operationPlan: } source: $.productAvailabilityById target: $.productById.subgraph1Only + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- @@ -267,6 +303,7 @@ operationPlan: } source: $.node target: $.productById.subgraph1Only + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph.yaml index cf96ac47d30..65a640cb7c4 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_List_Fields_That_Require_Data_From_Another_Subgraph.yaml @@ -174,6 +174,15 @@ sourceSchemas: }, { "__fusion_1_id": "UHJvZHVjdDo2" + }, + { + "__fusion_1_id": "UHJvZHVjdDox" + }, + { + "__fusion_1_id": "UHJvZHVjdDoy" + }, + { + "__fusion_1_id": "UHJvZHVjdDoz" } ] response: @@ -209,34 +218,6 @@ sourceSchemas: } } } - - request: - document: | - query Op_9ad18dbf_3( - $__fusion_2_id: ID! - ) { - node(id: $__fusion_2_id) { - __typename - ... on Product { - price - reviewCount - } - } - } - variables: | - [ - { - "__fusion_2_id": "UHJvZHVjdDox" - }, - { - "__fusion_2_id": "UHJvZHVjdDoy" - }, - { - "__fusion_2_id": "UHJvZHVjdDoz" - } - ] - response: - contentType: application/jsonl; charset=utf-8 - results: - | { "data": { @@ -305,7 +286,7 @@ operationPlan: } } - id: 2 - type: Operation + type: OperationBatch schema: B operation: | query Op_9ad18dbf_2( @@ -320,33 +301,13 @@ operationPlan: } } source: $.node - target: $.productsB + targets: + - $.productsB + - $.productsA + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- id dependencies: - id: 1 - - id: 3 - type: Operation - schema: B - operation: | - query Op_9ad18dbf_3( - $__fusion_2_id: ID! - ) { - node(id: $__fusion_2_id) { - __typename - ... on Product { - price - reviewCount - } - } - } - source: $.node - target: $.productsA - requirements: - - name: __fusion_2_id - selectionMap: >- - id - dependencies: - - id: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph.yaml index c758b6bdcf3..f06c1f74941 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.AspNetCore.Tests/v15/__snapshots__/DemoIntegrationTests.Same_Selection_On_Two_Object_Types_That_Require_Data_From_Another_Subgraph.yaml @@ -125,10 +125,16 @@ sourceSchemas: } } variables: | - { - "__fusion_1_id": "UHJvZHVjdDoz" - } + [ + { + "__fusion_1_id": "UHJvZHVjdDoz" + }, + { + "__fusion_1_id": "UHJvZHVjdDo0" + } + ] response: + contentType: application/jsonl; charset=utf-8 results: - | { @@ -139,24 +145,6 @@ sourceSchemas: } } } - - request: - document: | - query Op_e2224f1b_3( - $__fusion_2_id: ID! - ) { - node(id: $__fusion_2_id) { - __typename - ... on Product { - name - } - } - } - variables: | - { - "__fusion_2_id": "UHJvZHVjdDo0" - } - response: - results: - | { "data": { @@ -206,7 +194,7 @@ operationPlan: } } - id: 2 - type: Operation + type: OperationBatch schema: B operation: | query Op_e2224f1b_2( @@ -220,32 +208,13 @@ operationPlan: } } source: $.node - target: $.item2.product + targets: + - $.item2.product + - $.item1.product + batchingGroupId: 1 requirements: - name: __fusion_1_id selectionMap: >- id dependencies: - id: 1 - - id: 3 - type: Operation - schema: B - operation: | - query Op_e2224f1b_3( - $__fusion_2_id: ID! - ) { - node(id: $__fusion_2_id) { - __typename - ... on Product { - name - } - } - } - source: $.node - target: $.item1.product - requirements: - - name: __fusion_2_id - selectionMap: >- - id - dependencies: - - id: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/Clients/SourceSchemaRequestDispatcherTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/Clients/SourceSchemaRequestDispatcherTests.cs new file mode 100644 index 00000000000..22e1a2473e8 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/Clients/SourceSchemaRequestDispatcherTests.cs @@ -0,0 +1,421 @@ +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using HotChocolate.Execution; +using HotChocolate.Execution.Errors; +using HotChocolate.Features; +using HotChocolate.Fusion.Diagnostics; +using HotChocolate.Fusion.Execution; +using HotChocolate.Fusion.Execution.Clients; +using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Fusion.Types; +using HotChocolate.Language; +using Microsoft.Extensions.DependencyInjection; + +namespace HotChocolate.Fusion; + +public sealed class SourceSchemaRequestDispatcherTests : FusionTestBase +{ + [Fact] + public async Task ExecuteAsync_Ungrouped_Request_Remains_Pass_Through() + { + // arrange + var client = new TestSourceSchemaClient(); + await using var context = CreateContext(client); + var request = CreateRequest(nodeId: 1); + + // act + var response = await context.SourceSchemaScheduler.ExecuteAsync( + request, + CancellationToken.None); + + // assert + Assert.Equal(1, client.ExecuteCount); + Assert.Equal(0, client.ExecuteBatchCount); + Assert.IsType(response); + } + + [Fact] + public async Task ExecuteAsync_Grouped_Waits_Until_All_Submitted_Or_Skipped() + { + // arrange + var client = new TestSourceSchemaClient(); + await using var context = CreateContext(client); + + context.SourceSchemaDispatcher.RegisterGroup(7, [1, 2, 3]); + + // act + var first = context.SourceSchemaScheduler.ExecuteAsync( + CreateRequest(nodeId: 1, groupId: 7), + CancellationToken.None) + .AsTask(); + var second = context.SourceSchemaScheduler.ExecuteAsync( + CreateRequest(nodeId: 2, groupId: 7), + CancellationToken.None) + .AsTask(); + + await Task.Delay(50); + Assert.False(first.IsCompleted); + Assert.False(second.IsCompleted); + + context.SourceSchemaDispatcher.SkipNode(3); + + await first.WaitAsync(TimeSpan.FromSeconds(2)); + await second.WaitAsync(TimeSpan.FromSeconds(2)); + + // assert + Assert.Equal(0, client.ExecuteCount); + Assert.Equal(1, client.ExecuteBatchCount); + } + + [Fact] + public async Task ExecuteAsync_Cascaded_Skip_Does_Not_Deadlock() + { + // arrange + var client = new TestSourceSchemaClient(); + await using var context = CreateContext(client); + + context.SourceSchemaDispatcher.RegisterGroup(11, [10, 11, 12]); + + // act + var pending = context.SourceSchemaScheduler.ExecuteAsync( + CreateRequest(nodeId: 10, groupId: 11), + CancellationToken.None) + .AsTask(); + + context.SourceSchemaDispatcher.SkipNode(11); + context.SourceSchemaDispatcher.SkipNode(12); + + var response = await pending.WaitAsync(TimeSpan.FromSeconds(2)); + + // assert + Assert.Equal(1, client.ExecuteCount); + Assert.Equal(0, client.ExecuteBatchCount); + Assert.IsType(response); + } + + [Fact] + public async Task ExecuteAsync_Mixed_Grouped_And_Ungrouped_Dispatches_Both_Paths() + { + // arrange + var client = new TestSourceSchemaClient(); + await using var context = CreateContext(client); + + context.SourceSchemaDispatcher.RegisterGroup(13, [1, 2]); + + // act + var ungrouped = await context.SourceSchemaScheduler.ExecuteAsync( + CreateRequest(nodeId: 9), + CancellationToken.None); + + var groupedFirst = context.SourceSchemaScheduler.ExecuteAsync( + CreateRequest(nodeId: 1, groupId: 13), + CancellationToken.None) + .AsTask(); + var groupedSecond = context.SourceSchemaScheduler.ExecuteAsync( + CreateRequest(nodeId: 2, groupId: 13), + CancellationToken.None) + .AsTask(); + + await groupedFirst.WaitAsync(TimeSpan.FromSeconds(2)); + await groupedSecond.WaitAsync(TimeSpan.FromSeconds(2)); + + // assert + Assert.IsType(ungrouped); + Assert.Equal(1, client.ExecuteCount); + Assert.Equal(1, client.ExecuteBatchCount); + } + + [Fact] + public async Task ExecuteAsync_Grouped_Batch_Correlates_Responses_Positionally() + { + // arrange + var client = new TestSourceSchemaClient(); + var response1 = new TestResponse("batch-1"); + var response2 = new TestResponse("batch-2"); + client.OnBatch = _ => [response1, response2]; + + await using var context = CreateContext(client); + context.SourceSchemaDispatcher.RegisterGroup(17, [1, 2]); + + // act — node 1 submits first, node 2 second → responses[0] goes to node 1, responses[1] to node 2 + var firstTask = context.SourceSchemaScheduler.ExecuteAsync( + CreateRequest(nodeId: 1, groupId: 17), + CancellationToken.None) + .AsTask(); + var secondTask = context.SourceSchemaScheduler.ExecuteAsync( + CreateRequest(nodeId: 2, groupId: 17), + CancellationToken.None) + .AsTask(); + + var first = await firstTask.WaitAsync(TimeSpan.FromSeconds(2)); + var second = await secondTask.WaitAsync(TimeSpan.FromSeconds(2)); + + // assert — positional correlation: first submitted gets responses[0], second gets responses[1] + Assert.Same(response1, first); + Assert.Same(response2, second); + } + + [Fact] + public async Task Abort_Releases_Grouped_Waiters() + { + // arrange + var client = new TestSourceSchemaClient(); + await using var context = CreateContext(client); + context.SourceSchemaDispatcher.RegisterGroup(19, [1, 2]); + + var pending = context.SourceSchemaScheduler.ExecuteAsync( + CreateRequest(nodeId: 1, groupId: 19), + CancellationToken.None) + .AsTask(); + + // act + context.SourceSchemaDispatcher.Abort(new InvalidOperationException("aborted")); + + // assert + await Assert.ThrowsAsync( + async () => await pending.WaitAsync(TimeSpan.FromSeconds(2))); + } + + [Fact] + public async Task ExecuteAsync_Subscription_Request_Does_Not_Use_Group_Dispatch() + { + // arrange + var client = new TestSourceSchemaClient(); + await using var context = CreateContext(client); + context.SourceSchemaDispatcher.RegisterGroup(23, [1, 2]); + + var request = CreateRequest( + nodeId: 1, + groupId: 23, + operationType: OperationType.Subscription); + + // act + var response = await context.SourceSchemaScheduler.ExecuteAsync( + request, + CancellationToken.None); + + // assert + Assert.Equal(1, client.ExecuteCount); + Assert.Equal(0, client.ExecuteBatchCount); + Assert.IsType(response); + } + + [Fact] + public async Task ExecuteAsync_Grouped_When_ClientResolver_Throws_All_Waiters_Are_Faulted() + { + // arrange + await using var context = CreateContext(new FailingSourceSchemaClient()); + context.SourceSchemaDispatcher.RegisterGroup(29, [1, 2]); + + var first = context.SourceSchemaScheduler.ExecuteAsync( + CreateRequest(nodeId: 1, groupId: 29), + CancellationToken.None) + .AsTask(); + var second = context.SourceSchemaScheduler.ExecuteAsync( + CreateRequest(nodeId: 2, groupId: 29), + CancellationToken.None) + .AsTask(); + + // act/assert + var firstError = await Assert.ThrowsAsync( + async () => await first.WaitAsync(TimeSpan.FromSeconds(2))); + var secondError = await Assert.ThrowsAsync( + async () => await second.WaitAsync(TimeSpan.FromSeconds(2))); + + Assert.Equal("resolver-failed", firstError.Message); + Assert.Equal("resolver-failed", secondError.Message); + } + + private static OperationPlanContext CreateContext(ISourceSchemaClient client) + { + var schemaServices = new ServiceCollection() + .AddSingleton(new TestNodeIdParser()) + .AddSingleton( + NoopFusionExecutionDiagnosticEvents.Instance) + .AddSingleton(new DefaultErrorHandler([])) + .BuildServiceProvider(); + + var schemaFeatures = new FeatureCollection(); + schemaFeatures.Set(new FusionOptions()); + schemaFeatures.Set(new FusionRequestOptions()); + + var doc = ComposeSchemaDocument("type Query { hello: String }"); + var schema = FusionSchemaDefinition.Create(doc, schemaServices, schemaFeatures); + + var plan = PlanOperation(schema, "{ hello }"); + + var requestServices = new ServiceCollection() + .AddSingleton( + new TestClientScopeFactory(client)) + .BuildServiceProvider(); + + var request = OperationRequest.FromId("test-doc-id"); + + var requestContext = new PooledRequestContext(); + requestContext.Initialize( + schema, 0, request, 0, requestServices, CancellationToken.None); + requestContext.VariableValues = [VariableValueCollection.Empty]; + + return new OperationPlanContext( + requestContext, + VariableValueCollection.Empty, + plan, + new CancellationTokenSource()); + } + + private static SourceSchemaClientRequest CreateRequest( + int nodeId, + int? groupId = null, + OperationType operationType = OperationType.Query) + => new() + { + Node = new TestExecutionNode(nodeId), + SchemaName = "schema", + BatchingGroupId = groupId, + OperationType = operationType, + OperationSourceText = "query { __typename }", + Variables = [] + }; + + private sealed class TestNodeIdParser : INodeIdParser + { + public bool TryParseTypeName( + string id, + [NotNullWhen(true)] out string? typeName) + { + typeName = null; + return false; + } + } + + private sealed class TestClientScopeFactory( + ISourceSchemaClient client) : ISourceSchemaClientScopeFactory + { + public ISourceSchemaClientScope CreateScope(ISchemaDefinition schemaDefinition) + => new TestClientScope(client); + } + + private sealed class TestClientScope( + ISourceSchemaClient client) : ISourceSchemaClientScope + { + public ISourceSchemaClient GetClient( + string schemaName, OperationType operationType) + => client; + + public ValueTask DisposeAsync() + => ValueTask.CompletedTask; + } + + private sealed class TestSourceSchemaClient : ISourceSchemaClient + { + public int ExecuteCount { get; private set; } + + public int ExecuteBatchCount { get; private set; } + + public Func, + ImmutableArray>? OnBatch { get; set; } + + public SourceSchemaClientCapabilities Capabilities + => SourceSchemaClientCapabilities.RequestBatching + | SourceSchemaClientCapabilities.VariableBatching; + + public ValueTask ExecuteAsync( + OperationPlanContext context, + SourceSchemaClientRequest request, + CancellationToken cancellationToken) + { + ExecuteCount++; + + var response = new TestResponse($"single-{request.Node.Id}"); + return new ValueTask(response); + } + + public ValueTask> ExecuteBatchAsync( + OperationPlanContext context, + ImmutableArray requests, + CancellationToken cancellationToken) + { + ExecuteBatchCount++; + + if (OnBatch is not null) + { + return new ValueTask>( + OnBatch(requests)); + } + + var builder = ImmutableArray.CreateBuilder( + requests.Length); + + for (var i = 0; i < requests.Length; i++) + { + builder.Add(new TestResponse($"batch-{requests[i].Node.Id}")); + } + + return new ValueTask>( + builder.MoveToImmutable()); + } + + public ValueTask DisposeAsync() + => ValueTask.CompletedTask; + } + + private sealed class FailingSourceSchemaClient : ISourceSchemaClient + { + public SourceSchemaClientCapabilities Capabilities + => SourceSchemaClientCapabilities.RequestBatching + | SourceSchemaClientCapabilities.VariableBatching; + + public ValueTask ExecuteAsync( + OperationPlanContext context, + SourceSchemaClientRequest request, + CancellationToken cancellationToken) + => throw new InvalidOperationException("resolver-failed"); + + public ValueTask> ExecuteBatchAsync( + OperationPlanContext context, + ImmutableArray requests, + CancellationToken cancellationToken) + => throw new InvalidOperationException("resolver-failed"); + + public ValueTask DisposeAsync() + => ValueTask.CompletedTask; + } + + private sealed class TestResponse(string id) : SourceSchemaClientResponse + { + public string Id { get; } = id; + + public override Uri Uri => new("http://localhost/graphql"); + + public override string ContentType => "application/json"; + + public override bool IsSuccessful => true; + + public override async IAsyncEnumerable ReadAsResultStreamAsync( + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + yield break; + } + + public override void Dispose() + { + } + } + + private sealed class TestExecutionNode(int id) : ExecutionNode + { + public override int Id { get; } = id; + + public override ExecutionNodeType Type => ExecutionNodeType.Operation; + + public override ReadOnlySpan Conditions => []; + + public override string? SchemaName => null; + + protected override ValueTask OnExecuteAsync( + OperationPlanContext context, + CancellationToken cancellationToken = default) + => throw new NotSupportedException(); + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/Serialization/JsonOperationPlanSerializationTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/Serialization/JsonOperationPlanSerializationTests.cs index 20f802e68e1..af6f56047e9 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/Serialization/JsonOperationPlanSerializationTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Execution/Serialization/JsonOperationPlanSerializationTests.cs @@ -1,6 +1,7 @@ using System.Text; using System.Text.Encodings.Web; using System.Text.Json; +using System.Text.Json.Nodes; using HotChocolate.Buffers; using HotChocolate.Fusion.Execution.Nodes; using HotChocolate.Fusion.Execution.Nodes.Serialization; @@ -119,4 +120,62 @@ ... on Author { var parsedPlanFormatted = formatter.Format(parsedPlan); parsedPlanFormatted.MatchInlineSnapshot(Encoding.UTF8.GetString(buffer.WrittenSpan)); } + + [Fact] + public void Parse_Plan_Without_BatchingGroupId() + { + // arrange + var compositeSchema = CreateCompositeSchema(); + var originalPlan = PlanOperation( + compositeSchema, + """ + { + productBySlug(slug: "1") { + id + name + estimatedDelivery(postCode: "12345") + } + } + """); + + using var buffer = new PooledArrayWriter(); + var formatter = new JsonOperationPlanFormatter( + new JsonWriterOptions + { + Indented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }); + formatter.Format(buffer, originalPlan); + + var json = JsonNode.Parse(buffer.WrittenSpan)!; + var nodes = json["nodes"]!.AsArray(); + + foreach (var node in nodes) + { + if (node?["type"]?.GetValue() is "Operation") + { + node.AsObject().Remove("batchingGroupId"); + } + } + + var legacyPlanSource = Encoding.UTF8.GetBytes( + json.ToJsonString( + new JsonSerializerOptions + { + WriteIndented = true + })); + + // act + var compiler = new OperationCompiler( + compositeSchema, + new DefaultObjectPool>>( + new DefaultPooledObjectPolicy>>())); + var parser = new JsonOperationPlanParser(compiler); + var parsedPlan = parser.Parse(legacyPlanSource); + + // assert + Assert.All( + parsedPlan.AllNodes.OfType(), + node => Assert.Null(node.BatchingGroupId)); + } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/K6PlanTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/K6PlanTests.cs new file mode 100644 index 00000000000..ea4644368b1 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/K6PlanTests.cs @@ -0,0 +1,81 @@ +using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Fusion.Types; + +namespace HotChocolate.Fusion.Planning; + +public class K6PlanTests : FusionTestBase +{ + [Fact] + public void DeepNesting() + { + // arrange + var schema = BenchmarkSchema(); + + // act + var plan = PlanOperation(schema, FileResource.Open("k6.graphql")); + + // assert + MatchSnapshot(plan); + } + + [Fact] + public void DeepNesting_Grouped_Nodes_Stay_In_Same_Dependency_Depth() + { + // arrange + var schema = BenchmarkSchema(); + + // act + var plan = PlanOperation(schema, FileResource.Open("k6.graphql")); + var nodes = plan.AllNodes.ToDictionary(t => t.Id); + var depthLookup = new Dictionary(); + + foreach (var node in nodes.Values) + { + GetDepth(node, nodes, depthLookup); + } + + // assert + foreach (var grouping in plan.AllNodes + .OfType() + .Where(t => t.BatchingGroupId is not null) + .GroupBy(t => t.BatchingGroupId!.Value)) + { + var depths = grouping.Select(t => depthLookup[t.Id]).Distinct().ToArray(); + Assert.Single(depths); + } + } + + private static FusionSchemaDefinition BenchmarkSchema() + => ComposeSchema( + FileResource.Open("k6-accounts.graphqls"), + FileResource.Open("k6-inventory.graphqls"), + FileResource.Open("k6-products.graphqls"), + FileResource.Open("k6-reviews.graphqls")); + + private static int GetDepth( + ExecutionNode node, + IReadOnlyDictionary nodes, + Dictionary depthLookup) + { + if (depthLookup.TryGetValue(node.Id, out var depth)) + { + return depth; + } + + depth = 0; + + foreach (var dependency in node.Dependencies) + { + if (!nodes.TryGetValue(dependency.Id, out var dependencyNode)) + { + continue; + } + + var dependencyDepth = GetDepth(dependencyNode, nodes, depthLookup); + depth = Math.Max(depth, dependencyDepth + 1); + } + + depthLookup[node.Id] = depth; + return depth; + } +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/OperationPlannerBatchingGroupIdTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/OperationPlannerBatchingGroupIdTests.cs new file mode 100644 index 00000000000..7a535c4879b --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/OperationPlannerBatchingGroupIdTests.cs @@ -0,0 +1,423 @@ +using System.Collections.Immutable; +using System.Text; +using System.Text.Encodings.Web; +using System.Text.Json; +using System.Text.Json.Nodes; +using HotChocolate.Fusion.Execution.Nodes; +using HotChocolate.Fusion.Execution.Nodes.Serialization; +using HotChocolate.Fusion.Rewriters; +using HotChocolate.Fusion.Types; +using HotChocolate.Language; +using HotChocolate.Types; +using Microsoft.Extensions.ObjectPool; + +namespace HotChocolate.Fusion.Planning; + +public class OperationPlannerBatchingGroupIdTests : FusionTestBase +{ + [Fact] + public void Plan_With_RequestGrouping_Disabled_Assigns_No_BatchingGroupIds() + { + // arrange + var schema = CreateBatchingSchema(); + + // act + var plan = PlanOperation(schema, QueryWithRepeatedLookups, enableRequestGrouping: false); + + // assert + Assert.All( + plan.AllNodes.OfType(), + node => Assert.Null(node.BatchingGroupId)); + } + + [Fact] + public void Plan_With_RequestGrouping_Enabled_Assigns_Deterministic_BatchingGroupIds() + { + // arrange + var schema = CreateBatchingSchema(); + + // act + var plan1 = PlanOperation(schema, QueryWithRepeatedLookups, enableRequestGrouping: true); + var plan2 = PlanOperation(schema, QueryWithRepeatedLookups, enableRequestGrouping: true); + + // assert + // The two structurally equivalent schema-b lookups are merged into one + // OperationBatchExecutionNode by the dedup optimization, but the BatchingGroupId + // is retained from the pre-merge assignment. + var schemaBBatchNode = Assert.Single( + plan1.AllNodes.OfType(), + t => t.SchemaName == "b"); + Assert.True(schemaBBatchNode.BatchingGroupId.HasValue); + Assert.Equal(2, schemaBBatchNode.Targets.Length); + + // BatchingGroupIds must be deterministic across plan runs. + var plan1Ids = plan1.AllNodes + .OfType() + .Select(t => t.BatchingGroupId) + .OrderBy(id => id) + .ToArray(); + var plan2Ids = plan2.AllNodes + .OfType() + .Select(t => t.BatchingGroupId) + .OrderBy(id => id) + .ToArray(); + Assert.Equal(plan1Ids, plan2Ids); + } + + [Fact] + public void CreateBatchingGroupLookup_Dependent_Query_Nodes_Do_Not_Share_Group() + { + // arrange + var schema = CreateBatchingSchema(); + var queryType = schema.Types["Query"]; + var queryDefinition = ParseOperationDefinition( + """ + query Step($id: ID!) { + productById(id: $id) { + rating + } + } + """); + + var steps = ImmutableList.Create( + CreateStep(1, queryDefinition, queryType, "b", SelectionPath.Parse("$.a"), SelectionPath.Parse("$.productById")), + CreateStep(2, queryDefinition, queryType, "b", SelectionPath.Parse("$.b"), SelectionPath.Parse("$.productById")), + CreateStep(3, queryDefinition, queryType, "b", SelectionPath.Parse("$.c"), SelectionPath.Parse("$.productById")), + CreateStep(10, queryDefinition, queryType, "b", SelectionPath.Parse("$.x"), SelectionPath.Parse("$.productById")), + CreateStep(11, queryDefinition, queryType, "b", SelectionPath.Parse("$.y"), SelectionPath.Parse("$.productById")), + CreateStep(12, queryDefinition, queryType, "b", SelectionPath.Parse("$.z"), SelectionPath.Parse("$.productById"))); + + var dependencyLookup = new Dictionary> + { + [2] = [1], + [3] = [2], + [11] = [10], + [12] = [11] + }; + + // act + var lookup = OperationPlanner.CreateBatchingGroupLookup( + steps, + dependencyLookup, + enableRequestGrouping: true); + + // assert + Assert.Equal(lookup[1], lookup[10]); + Assert.Equal(lookup[2], lookup[11]); + Assert.Equal(lookup[3], lookup[12]); + Assert.NotEqual(lookup[1], lookup[2]); + Assert.NotEqual(lookup[1], lookup[3]); + Assert.NotEqual(lookup[2], lookup[3]); + } + + [Fact] + public void CreateBatchingGroupLookup_Nodes_From_Different_Schemas_Do_Not_Share_Group() + { + // arrange + var schema = CreateBatchingSchema(); + var queryType = schema.Types["Query"]; + var queryDefinition = ParseOperationDefinition( + """ + query Step($id: ID!) { + productById(id: $id) { + rating + } + } + """); + + var steps = ImmutableList.Create( + CreateStep(1, queryDefinition, queryType, "b", SelectionPath.Parse("$.a"), SelectionPath.Parse("$.productById")), + CreateStep(2, queryDefinition, queryType, "b", SelectionPath.Parse("$.b"), SelectionPath.Parse("$.productById")), + CreateStep(3, queryDefinition, queryType, "c", SelectionPath.Parse("$.c"), SelectionPath.Parse("$.productById")), + CreateStep(4, queryDefinition, queryType, "c", SelectionPath.Parse("$.d"), SelectionPath.Parse("$.productById"))); + + // act + var lookup = OperationPlanner.CreateBatchingGroupLookup( + steps, + new Dictionary>(), + enableRequestGrouping: true); + + // assert + Assert.Equal(lookup[1], lookup[2]); + Assert.Equal(lookup[3], lookup[4]); + Assert.NotEqual(lookup[1], lookup[3]); + } + + [Fact] + public void Plan_NonQuery_Operation_Nodes_Do_Not_Get_BatchingGroupId() + { + // arrange + var schema = ComposeSchema( + """ + # name: a + schema { + query: Query + mutation: Mutation + subscription: Subscription + } + + type Query { + _empty: String + } + + type Mutation { + doWork: Int! + } + + type Subscription { + onWork: Int! + } + """); + + // act + var mutationPlan = PlanOperation( + schema, + """ + mutation { + doWork + } + """, + enableRequestGrouping: true); + var subscriptionPlan = PlanOperation( + schema, + """ + subscription { + onWork + } + """, + enableRequestGrouping: true); + + // assert + var mutationNode = Assert.Single(mutationPlan.AllNodes.OfType()); + Assert.Equal(OperationType.Mutation, mutationNode.Operation.Type); + Assert.Null(mutationNode.BatchingGroupId); + + var subscriptionNode = Assert.Single(subscriptionPlan.AllNodes.OfType()); + Assert.Equal(OperationType.Subscription, subscriptionNode.Operation.Type); + Assert.Null(subscriptionNode.BatchingGroupId); + } + + [Fact] + public void Serialization_Includes_BatchingGroupId_When_Present() + { + // arrange + var schema = CreateBatchingSchema(); + var plan = PlanOperation(schema, QueryWithoutRepeatedLookups, enableRequestGrouping: false); + var initialJson = new JsonOperationPlanFormatter( + new JsonWriterOptions + { + Indented = true, + Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping + }).Format(plan); + var jsonDocument = JsonNode.Parse(initialJson)!; + var operationNode = jsonDocument["nodes"]! + .AsArray() + .Select(t => t!.AsObject()) + .First(t => t["type"]!.GetValue() is "Operation"); + operationNode["batchingGroupId"] = 42; + + var pool = new DefaultObjectPool>>( + new DefaultPooledObjectPolicy>>()); + var parser = new JsonOperationPlanParser(new OperationCompiler(schema, pool)); + plan = parser.Parse( + Encoding.UTF8.GetBytes( + jsonDocument.ToJsonString( + new JsonSerializerOptions + { + WriteIndented = true + }))); + + // act + var yaml = new YamlOperationPlanFormatter().Format(plan); + var json = new JsonOperationPlanFormatter().Format(plan); + + // assert + Assert.Contains("batchingGroupId:", yaml, StringComparison.Ordinal); + Assert.Contains("\"batchingGroupId\":", json, StringComparison.Ordinal); + Assert.DoesNotContain("batchingGroupId: null", yaml, StringComparison.Ordinal); + Assert.DoesNotContain("\"batchingGroupId\": null", json, StringComparison.Ordinal); + } + + [Fact] + public void Serialization_Omits_BatchingGroupId_When_Null() + { + // arrange + var schema = CreateBatchingSchema(); + var plan = PlanOperation(schema, QueryWithoutRepeatedLookups, enableRequestGrouping: false); + + // act + var yaml = new YamlOperationPlanFormatter().Format(plan); + var json = new JsonOperationPlanFormatter().Format(plan); + + // assert + Assert.All( + plan.AllNodes.OfType(), + node => Assert.Null(node.BatchingGroupId)); + Assert.DoesNotContain("batchingGroupId:", yaml, StringComparison.Ordinal); + Assert.DoesNotContain("\"batchingGroupId\":", json, StringComparison.Ordinal); + } + + [Fact] + public void Snapshot_Plan_Shows_BatchingGroup_When_Group_Is_Created() + { + // arrange + var schema = CreateBatchingSchema(); + var plan = PlanOperation(schema, QueryWithRepeatedLookups, enableRequestGrouping: true); + + // act + var yaml = new YamlOperationPlanFormatter().Format(plan); + + // assert + Assert.Contains("batchingGroupId:", yaml, StringComparison.Ordinal); + Assert.DoesNotContain("batchingGroupId: null", yaml, StringComparison.Ordinal); + MatchSnapshot(plan); + } + + private static OperationPlan PlanOperation( + FusionSchemaDefinition schema, + string operationText, + bool enableRequestGrouping) + { + var pool = new DefaultObjectPool>>( + new DefaultPooledObjectPolicy>>()); + + var operationDoc = Utf8GraphQLParser.Parse(operationText); + + var rewriter = new DocumentRewriter(schema); + var rewritten = rewriter.RewriteDocument(operationDoc, operationName: null); + var operation = rewritten.Definitions.OfType().First(); + + var compiler = new OperationCompiler(schema, pool); + var planner = new OperationPlanner( + schema, + compiler, + new OperationPlannerOptions + { + EnableRequestGrouping = enableRequestGrouping + }); + const string id = "123456789101112"; + return planner.CreatePlan(id, id, id, operation); + } + + private static string CreateNodeSignature(OperationExecutionNode node) + { + return string.Concat( + node.SchemaName ?? "__dynamic__", + "|", + node.Operation.Type.ToString(), + "|", + node.Operation.Hash, + "|", + node.Source.ToString(), + "|", + node.Target.ToString()); + } + + private static OperationDefinitionNode ParseOperationDefinition( + string operationSourceText) + { + return Utf8GraphQLParser + .Parse(operationSourceText) + .Definitions + .OfType() + .Single(); + } + + private static OperationPlanStep CreateStep( + int id, + OperationDefinitionNode definition, + ITypeDefinition type, + string? schemaName, + SelectionPath target, + SelectionPath source) + { + return new OperationPlanStep + { + Id = id, + Definition = definition, + Type = type, + RootSelectionSetId = 1, + SelectionSets = [1], + SchemaName = schemaName, + Target = target, + Source = source + }; + } + + private static FusionSchemaDefinition CreateBatchingSchema() + { + return ComposeSchema( + """ + # name: a + schema { + query: Query + } + + type Query { + first: Product + second: Product + } + + type Product @key(fields: "id") { + id: ID! + } + """, + """ + # name: b + schema { + query: Query + } + + type Query { + productById(id: ID! @is(field: "id")): Product @lookup @internal + } + + type Product @key(fields: "id") { + id: ID! + rating: Int! + } + """, + """ + # name: c + schema { + query: Query + } + + type Query { + productById(id: ID! @is(field: "id")): Product @lookup @internal + } + + type Product @key(fields: "id") { + id: ID! + deliveryEstimate: Int! + } + """); + } + + private const string QueryWithRepeatedLookups = + """ + { + first { + id + rating + deliveryEstimate + } + second { + id + rating + deliveryEstimate + } + } + """; + + private const string QueryWithoutRepeatedLookups = + """ + { + first { + id + rating + deliveryEstimate + } + } + """; +} diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/K6PlanTests.DeepNesting.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/K6PlanTests.DeepNesting.yaml new file mode 100644 index 00000000000..9f3a2cdd1e6 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/K6PlanTests.DeepNesting.yaml @@ -0,0 +1,393 @@ +operation: + - document: | + query TestQuery { + users { + id + id @fusion__requirement + username + name + reviews { + id + body + product { + inStock + name + price + price @fusion__requirement + shippingEstimate + upc + upc @fusion__requirement + weight + weight @fusion__requirement + reviews { + id + body + author { + id + id @fusion__requirement + username + name + reviews { + id + body + product { + inStock + name + price + price @fusion__requirement + shippingEstimate + upc + upc @fusion__requirement + weight + weight @fusion__requirement + } + } + } + } + } + } + } + topProducts(first: 5) { + inStock + name + price + price @fusion__requirement + shippingEstimate + upc + upc @fusion__requirement + weight + weight @fusion__requirement + reviews { + id + body + author { + id + id @fusion__requirement + username + name + reviews { + id + body + product { + inStock + name + price + price @fusion__requirement + shippingEstimate + upc + upc @fusion__requirement + weight + weight @fusion__requirement + } + } + } + } + } + } + name: TestQuery + hash: 123456789101112 + searchSpace: 3 + expandedNodes: 14 +nodes: + - id: 1 + type: Operation + schema: c + operation: | + query TestQuery_123456789101112_1 { + topProducts(first: 5) { + name + price + upc + weight + } + } + - id: 2 + type: Operation + schema: a + operation: | + query TestQuery_123456789101112_2 { + users { + id + username + name + } + } + - id: 3 + type: Operation + schema: d + operation: | + query TestQuery_123456789101112_3( + $__fusion_1_id: ID! + ) { + user(id: $__fusion_1_id) { + reviews { + id + body + product { + upc + reviews { + id + body + author { + id + reviews { + id + body + product { + upc + } + } + } + } + } + } + } + } + source: $.user + target: $.users + batchingGroupId: 4 + requirements: + - name: __fusion_1_id + selectionMap: >- + id + dependencies: + - id: 2 + - id: 4 + type: OperationBatch + schema: c + operation: | + query TestQuery_123456789101112_4( + $__fusion_2_upc: ID! + ) { + product(upc: $__fusion_2_upc) { + name + price + weight + } + } + source: $.product + targets: + - $.users.reviews.product + - $.users.reviews.product.reviews.author.reviews.product + batchingGroupId: 3 + requirements: + - name: __fusion_2_upc + selectionMap: >- + upc + dependencies: + - id: 3 + - id: 5 + type: Operation + schema: b + operation: | + query TestQuery_123456789101112_5( + $__fusion_3_upc: ID! + $__fusion_4_price: Long! + $__fusion_4_weight: Long! + ) { + productByUpc(upc: $__fusion_3_upc) { + inStock + shippingEstimate(weight: $__fusion_4_weight, price: $__fusion_4_price) + } + } + source: $.productByUpc + target: $.users.reviews.product + batchingGroupId: 2 + requirements: + - name: __fusion_3_upc + selectionMap: >- + upc + - name: __fusion_4_price + selectionMap: >- + price + - name: __fusion_4_weight + selectionMap: >- + weight + dependencies: + - id: 3 + - id: 4 + - id: 6 + type: Operation + schema: a + operation: | + query TestQuery_123456789101112_6( + $__fusion_5_id: ID! + ) { + user(id: $__fusion_5_id) { + username + name + } + } + source: $.user + target: $.users.reviews.product.reviews.author + batchingGroupId: 1 + requirements: + - name: __fusion_5_id + selectionMap: >- + id + dependencies: + - id: 3 + - id: 8 + type: Operation + schema: b + operation: | + query TestQuery_123456789101112_8( + $__fusion_7_upc: ID! + $__fusion_8_price: Long! + $__fusion_8_weight: Long! + ) { + productByUpc(upc: $__fusion_7_upc) { + inStock + shippingEstimate(weight: $__fusion_8_weight, price: $__fusion_8_price) + } + } + source: $.productByUpc + target: $.users.reviews.product.reviews.author.reviews.product + batchingGroupId: 2 + requirements: + - name: __fusion_7_upc + selectionMap: >- + upc + - name: __fusion_8_price + selectionMap: >- + price + - name: __fusion_8_weight + selectionMap: >- + weight + dependencies: + - id: 3 + - id: 4 + - id: 9 + type: Operation + schema: d + operation: | + query TestQuery_123456789101112_9( + $__fusion_9_upc: ID! + ) { + product(upc: $__fusion_9_upc) { + reviews { + id + body + author { + id + reviews { + id + body + product { + upc + } + } + } + } + } + } + source: $.product + target: $.topProducts + batchingGroupId: 4 + requirements: + - name: __fusion_9_upc + selectionMap: >- + upc + dependencies: + - id: 1 + - id: 10 + type: Operation + schema: b + operation: | + query TestQuery_123456789101112_10( + $__fusion_10_upc: ID! + $__fusion_11_price: Long! + $__fusion_11_weight: Long! + ) { + productByUpc(upc: $__fusion_10_upc) { + inStock + shippingEstimate(weight: $__fusion_11_weight, price: $__fusion_11_price) + } + } + source: $.productByUpc + target: $.topProducts + requirements: + - name: __fusion_10_upc + selectionMap: >- + upc + - name: __fusion_11_price + selectionMap: >- + price + - name: __fusion_11_weight + selectionMap: >- + weight + dependencies: + - id: 1 + - id: 11 + type: Operation + schema: a + operation: | + query TestQuery_123456789101112_11( + $__fusion_12_id: ID! + ) { + user(id: $__fusion_12_id) { + username + name + } + } + source: $.user + target: $.topProducts.reviews.author + batchingGroupId: 1 + requirements: + - name: __fusion_12_id + selectionMap: >- + id + dependencies: + - id: 9 + - id: 12 + type: Operation + schema: c + operation: | + query TestQuery_123456789101112_12( + $__fusion_13_upc: ID! + ) { + product(upc: $__fusion_13_upc) { + name + price + weight + } + } + source: $.product + target: $.topProducts.reviews.author.reviews.product + batchingGroupId: 3 + requirements: + - name: __fusion_13_upc + selectionMap: >- + upc + dependencies: + - id: 9 + - id: 13 + type: Operation + schema: b + operation: | + query TestQuery_123456789101112_13( + $__fusion_14_upc: ID! + $__fusion_15_price: Long! + $__fusion_15_weight: Long! + ) { + productByUpc(upc: $__fusion_14_upc) { + inStock + shippingEstimate(weight: $__fusion_15_weight, price: $__fusion_15_price) + } + } + source: $.productByUpc + target: $.topProducts.reviews.author.reviews.product + batchingGroupId: 2 + requirements: + - name: __fusion_14_upc + selectionMap: >- + upc + - name: __fusion_15_price + selectionMap: >- + price + - name: __fusion_15_weight + selectionMap: >- + weight + dependencies: + - id: 9 + - id: 12 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/OperationPlannerBatchingGroupIdTests.Snapshot_Plan_Shows_BatchingGroup_When_Group_Is_Created.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/OperationPlannerBatchingGroupIdTests.Snapshot_Plan_Shows_BatchingGroup_When_Group_Is_Created.yaml new file mode 100644 index 00000000000..e57a477a0f7 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/OperationPlannerBatchingGroupIdTests.Snapshot_Plan_Shows_BatchingGroup_When_Group_Is_Created.yaml @@ -0,0 +1,76 @@ +operation: + - document: | + { + first { + id + id @fusion__requirement + rating + deliveryEstimate + } + second { + id + id @fusion__requirement + rating + deliveryEstimate + } + } + hash: 123456789101112 + searchSpace: 2 + expandedNodes: 5 +nodes: + - id: 1 + type: Operation + schema: a + operation: | + query Op_123456789101112_1 { + first { + id + } + second { + id + } + } + - id: 2 + type: OperationBatch + schema: b + operation: | + query Op_123456789101112_2( + $__fusion_1_id: ID! + ) { + productById(id: $__fusion_1_id) { + rating + } + } + source: $.productById + targets: + - $.second + - $.first + batchingGroupId: 1 + requirements: + - name: __fusion_1_id + selectionMap: >- + id + dependencies: + - id: 1 + - id: 3 + type: OperationBatch + schema: c + operation: | + query Op_123456789101112_3( + $__fusion_2_id: ID! + ) { + productById(id: $__fusion_2_id) { + deliveryEstimate + } + } + source: $.productById + targets: + - $.second + - $.first + batchingGroupId: 2 + requirements: + - name: __fusion_2_id + selectionMap: >- + id + dependencies: + - id: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/PlannerAdvancedAdaptationTests.Issues_Issue_281.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/PlannerAdvancedAdaptationTests.Issues_Issue_281.yaml index 86435c4ea47..2111af01241 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/PlannerAdvancedAdaptationTests.Issues_Issue_281.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/PlannerAdvancedAdaptationTests.Issues_Issue_281.yaml @@ -102,6 +102,7 @@ nodes: } source: $.productById target: $.viewer.review.product + batchingGroupId: 1 requirements: - name: __fusion_3_id selectionMap: >- @@ -121,6 +122,7 @@ nodes: } source: $.productById target: $.viewer.review.product + batchingGroupId: 1 requirements: - name: __fusion_4_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/PlannerBehaviorTests.Fragments_Fragment_Spread.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/PlannerBehaviorTests.Fragments_Fragment_Spread.yaml index 79b66f09e85..032c6bf6e51 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/PlannerBehaviorTests.Fragments_Fragment_Spread.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/PlannerBehaviorTests.Fragments_Fragment_Spread.yaml @@ -59,6 +59,7 @@ nodes: } source: $.productById target: $.products + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- @@ -78,6 +79,7 @@ nodes: } source: $.productById target: $.products + batchingGroupId: 1 requirements: - name: __fusion_3_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/PlannerBehaviorTests.Fragments_Simple_Inline_Fragment.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/PlannerBehaviorTests.Fragments_Simple_Inline_Fragment.yaml index 79b66f09e85..032c6bf6e51 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/PlannerBehaviorTests.Fragments_Simple_Inline_Fragment.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/PlannerBehaviorTests.Fragments_Simple_Inline_Fragment.yaml @@ -59,6 +59,7 @@ nodes: } source: $.productById target: $.products + batchingGroupId: 1 requirements: - name: __fusion_2_id selectionMap: >- @@ -78,6 +79,7 @@ nodes: } source: $.productById target: $.products + batchingGroupId: 1 requirements: - name: __fusion_3_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Multiple_Plain_Field_And_Requires_With_Args_That_Conflicts.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Multiple_Plain_Field_And_Requires_With_Args_That_Conflicts.yaml index be200e0e07b..2e2a804b08e 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Multiple_Plain_Field_And_Requires_With_Args_That_Conflicts.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Multiple_Plain_Field_And_Requires_With_Args_That_Conflicts.yaml @@ -37,6 +37,7 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 1 requirements: - name: __fusion_1_otherField selectionMap: >- @@ -61,6 +62,7 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 2 requirements: - name: __fusion_3_id selectionMap: >- @@ -80,6 +82,7 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 2 requirements: - name: __fusion_4_id selectionMap: >- @@ -100,6 +103,7 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 1 requirements: - name: __fusion_5_otherField selectionMap: >- @@ -123,6 +127,7 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 2 requirements: - name: __fusion_7_id selectionMap: >- @@ -142,6 +147,7 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 2 requirements: - name: __fusion_8_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Multiple_Plain_Field_And_Requires_With_Args_That_Does_Not_Conflicts_Should_Merge.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Multiple_Plain_Field_And_Requires_With_Args_That_Does_Not_Conflicts_Should_Merge.yaml index a633a3ab823..0bb13489cfb 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Multiple_Plain_Field_And_Requires_With_Args_That_Does_Not_Conflicts_Should_Merge.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Multiple_Plain_Field_And_Requires_With_Args_That_Does_Not_Conflicts_Should_Merge.yaml @@ -47,7 +47,7 @@ nodes: - id: 3 - id: 4 - id: 3 - type: Operation + type: OperationBatch schema: b operation: | query Op_123456789101112_3( @@ -58,7 +58,10 @@ nodes: } } source: $.testById - target: $.test + targets: + - $.test + - $.test + batchingGroupId: 1 requirements: - name: __fusion_3_id selectionMap: >- @@ -78,28 +81,10 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 1 requirements: - name: __fusion_4_id selectionMap: >- id dependencies: - id: 1 - - id: 5 - type: Operation - schema: b - operation: | - query Op_123456789101112_5( - $__fusion_5_id: ID! - ) { - testById(id: $__fusion_5_id) { - otherField - } - } - source: $.testById - target: $.test - requirements: - - name: __fusion_5_id - selectionMap: >- - id - dependencies: - - id: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Multiple_Requires_With_Args_That_Conflicts.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Multiple_Requires_With_Args_That_Conflicts.yaml index 4258a74cdf7..1a452d6af3f 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Multiple_Requires_With_Args_That_Conflicts.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Multiple_Requires_With_Args_That_Conflicts.yaml @@ -36,6 +36,7 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 1 requirements: - name: __fusion_1_otherField selectionMap: >- @@ -60,6 +61,7 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 2 requirements: - name: __fusion_3_id selectionMap: >- @@ -79,6 +81,7 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 2 requirements: - name: __fusion_4_id selectionMap: >- @@ -99,6 +102,7 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 1 requirements: - name: __fusion_5_otherField selectionMap: >- @@ -122,6 +126,7 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 2 requirements: - name: __fusion_7_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Requires_With_Arguments.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Requires_With_Arguments.yaml index 210e1db8b27..ec002aca4da 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Requires_With_Arguments.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Requires_With_Arguments.yaml @@ -28,6 +28,7 @@ nodes: } } } + batchingGroupId: 1 - id: 2 type: Operation schema: b @@ -83,3 +84,4 @@ nodes: } } } + batchingGroupId: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Simple_Requires_Arguments.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Simple_Requires_Arguments.yaml index 937244f9ab0..a722d6d85f1 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Simple_Requires_Arguments.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementArgumentTests.Simple_Requires_Arguments.yaml @@ -58,6 +58,7 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 1 requirements: - name: __fusion_3_id selectionMap: >- @@ -77,6 +78,7 @@ nodes: } source: $.testById target: $.test + batchingGroupId: 1 requirements: - name: __fusion_4_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementChainTests.Requires_Circular_2.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementChainTests.Requires_Circular_2.yaml index 9367d37717d..317f4301a48 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementChainTests.Requires_Circular_2.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementChainTests.Requires_Circular_2.yaml @@ -64,6 +64,7 @@ nodes: } source: $.postById target: $.feed + batchingGroupId: 1 requirements: - name: __fusion_3_id selectionMap: >- @@ -86,6 +87,7 @@ nodes: } source: $.postById target: $.feed + batchingGroupId: 1 requirements: - name: __fusion_5_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementChainTests.Requires_Requires_Many.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementChainTests.Requires_Requires_Many.yaml index f91798f1d73..cd6ea3d0c70 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementChainTests.Requires_Requires_Many.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementChainTests.Requires_Requires_Many.yaml @@ -65,6 +65,7 @@ nodes: } source: $.productById target: $.product + batchingGroupId: 3 requirements: - name: __fusion_3_isExpensiveWithDiscount selectionMap: >- @@ -89,6 +90,7 @@ nodes: } source: $.productById target: $.product + batchingGroupId: 1 requirements: - name: __fusion_5_id selectionMap: >- @@ -112,6 +114,7 @@ nodes: } source: $.productById target: $.product + batchingGroupId: 3 requirements: - name: __fusion_7_isExpensiveWithDiscount selectionMap: >- @@ -136,6 +139,7 @@ nodes: } source: $.productById target: $.product + batchingGroupId: 1 requirements: - name: __fusion_10_hasDiscount selectionMap: >- @@ -159,6 +163,7 @@ nodes: } source: $.productById target: $.product + batchingGroupId: 4 requirements: - name: __fusion_11_isExpensive selectionMap: >- @@ -170,7 +175,7 @@ nodes: - id: 1 - id: 9 - id: 9 - type: Operation + type: OperationBatch schema: c operation: | query Op_123456789101112_9( @@ -182,7 +187,10 @@ nodes: } } source: $.productById - target: $.product + targets: + - $.product + - $.product + batchingGroupId: 2 requirements: - name: __fusion_13_id selectionMap: >- @@ -207,6 +215,7 @@ nodes: } source: $.productById target: $.product + batchingGroupId: 4 requirements: - name: __fusion_15_isExpensive selectionMap: >- @@ -216,31 +225,7 @@ nodes: id dependencies: - id: 1 - - id: 11 - - id: 11 - type: Operation - schema: c - operation: | - query Op_123456789101112_11( - $__fusion_17_id: ID! - $__fusion_18_price: Float! - ) { - productById(id: $__fusion_17_id) { - isExpensive(price: $__fusion_18_price) - } - } - source: $.productById - target: $.product - requirements: - - name: __fusion_17_id - selectionMap: >- - id - - name: __fusion_18_price - selectionMap: >- - price - dependencies: - - id: 1 - - id: 2 + - id: 9 - id: 12 type: Operation schema: c @@ -257,6 +242,7 @@ nodes: } source: $.productById target: $.product + batchingGroupId: 2 requirements: - name: __fusion_19_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementChainTests.Requires_Requires_Two_Fields_Same_Requirement_Different_Order.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementChainTests.Requires_Requires_Two_Fields_Same_Requirement_Different_Order.yaml index 70cfc61e63a..912f86423ae 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementChainTests.Requires_Requires_Two_Fields_Same_Requirement_Different_Order.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementChainTests.Requires_Requires_Two_Fields_Same_Requirement_Different_Order.yaml @@ -90,6 +90,7 @@ nodes: } source: $.productById target: $.product + batchingGroupId: 1 requirements: - name: __fusion_6_price selectionMap: >- @@ -163,6 +164,7 @@ nodes: } source: $.productById target: $.product + batchingGroupId: 1 requirements: - name: __fusion_11_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementParityTests.Keys_Mashup.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementParityTests.Keys_Mashup.yaml index 0bf9720536d..d45b9afbde1 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementParityTests.Keys_Mashup.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementParityTests.Keys_Mashup.yaml @@ -65,7 +65,7 @@ nodes: - id: 3 - id: 4 - id: 3 - type: Operation + type: OperationBatch schema: a operation: | query Op_123456789101112_3( @@ -76,7 +76,10 @@ nodes: } } source: $.aById - target: $.b.a + targets: + - $.b.a + - $.b.a + batchingGroupId: 1 requirements: - name: __fusion_3_id selectionMap: >- @@ -100,28 +103,10 @@ nodes: } source: $.aById target: $.b.a + batchingGroupId: 1 requirements: - name: __fusion_4_id selectionMap: >- id dependencies: - id: 1 - - id: 5 - type: Operation - schema: a - operation: | - query Op_123456789101112_5( - $__fusion_5_id: ID! - ) { - aById(id: $__fusion_5_id) { - name - } - } - source: $.aById - target: $.b.a - requirements: - - name: __fusion_5_id - selectionMap: >- - id - dependencies: - - id: 1 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementParityTests.Two_Same_Service_Calls_With_Args_Conflicts.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementParityTests.Two_Same_Service_Calls_With_Args_Conflicts.yaml index 65487780191..b923bbc2faf 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementParityTests.Two_Same_Service_Calls_With_Args_Conflicts.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementParityTests.Two_Same_Service_Calls_With_Args_Conflicts.yaml @@ -36,6 +36,7 @@ nodes: } source: $.productByUpc target: $.products + batchingGroupId: 1 requirements: - name: __fusion_1_price selectionMap: >- @@ -60,6 +61,7 @@ nodes: } source: $.productByUpc target: $.products + batchingGroupId: 2 requirements: - name: __fusion_3_upc selectionMap: >- @@ -79,6 +81,7 @@ nodes: } source: $.productByUpc target: $.products + batchingGroupId: 2 requirements: - name: __fusion_4_upc selectionMap: >- @@ -99,6 +102,7 @@ nodes: } source: $.productByUpc target: $.products + batchingGroupId: 1 requirements: - name: __fusion_5_price selectionMap: >- @@ -122,6 +126,7 @@ nodes: } source: $.productByUpc target: $.products + batchingGroupId: 2 requirements: - name: __fusion_7_upc selectionMap: >- @@ -141,6 +146,7 @@ nodes: } source: $.productByUpc target: $.products + batchingGroupId: 2 requirements: - name: __fusion_8_upc selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementTests.Requirement_Directive_Leaks_Into_SourceSchema_Request_Shop.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementTests.Requirement_Directive_Leaks_Into_SourceSchema_Request_Shop.yaml index 994deab2c61..e094668e77f 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementTests.Requirement_Directive_Leaks_Into_SourceSchema_Request_Shop.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/RequirementTests.Requirement_Directive_Leaks_Into_SourceSchema_Request_Shop.yaml @@ -139,6 +139,7 @@ nodes: } source: $.productById target: $.users.nodes.reviews.nodes.product + batchingGroupId: 2 requirements: - name: __fusion_2_id selectionMap: >- @@ -158,6 +159,7 @@ nodes: } source: $.productByIdAsync target: $.users.nodes.reviews.nodes.product + batchingGroupId: 1 requirements: - name: __fusion_3_id selectionMap: >- @@ -233,6 +235,7 @@ nodes: } source: $.productByIdAsync target: $.users.nodes.reviews.nodes.product.reviews.nodes.author.reviews.nodes.product + batchingGroupId: 1 requirements: - name: __fusion_7_id selectionMap: >- @@ -252,6 +255,7 @@ nodes: } source: $.productById target: $.users.nodes.reviews.nodes.product.reviews.nodes.author.reviews.nodes.product + batchingGroupId: 2 requirements: - name: __fusion_8_id selectionMap: >- diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/ShopPlanningTests.Medium_Query_With_Aliases.yaml b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/ShopPlanningTests.Medium_Query_With_Aliases.yaml index 304cc86cb3c..f40f9e45d70 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/ShopPlanningTests.Medium_Query_With_Aliases.yaml +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/Planning/__snapshots__/ShopPlanningTests.Medium_Query_With_Aliases.yaml @@ -175,7 +175,7 @@ nodes: dependencies: - id: 1 - id: 3 - type: Operation + type: OperationBatch schema: e operation: | query Op_123456789101112_3( @@ -193,7 +193,10 @@ nodes: } } source: $.productById - target: $.products.nodes.b.nodes.product + targets: + - $.products.nodes.b.nodes.product + - $.products.nodes.reviews.nodes.product + batchingGroupId: 4 requirements: - name: __fusion_2_id selectionMap: >- @@ -201,7 +204,7 @@ nodes: dependencies: - id: 2 - id: 4 - type: Operation + type: OperationBatch schema: b operation: | query Op_123456789101112_4( @@ -217,7 +220,10 @@ nodes: } } source: $.productByIdAsync - target: $.products.nodes.b.nodes.product + targets: + - $.products.nodes.b.nodes.product + - $.products.nodes.reviews.nodes.product + batchingGroupId: 3 requirements: - name: __fusion_3_id selectionMap: >- @@ -238,6 +244,7 @@ nodes: } source: $.productById target: $.products.nodes.b.nodes.product + batchingGroupId: 7 requirements: - name: __fusion_4_id selectionMap: >- @@ -274,6 +281,7 @@ nodes: } source: $.productById target: $.products.nodes.b.nodes.product.item.product + batchingGroupId: 6 requirements: - name: __fusion_6_id selectionMap: >- @@ -299,6 +307,7 @@ nodes: } source: $.productById target: $.products.nodes.b.nodes.product.item.product + batchingGroupId: 5 requirements: - name: __fusion_7_id selectionMap: >- @@ -319,6 +328,7 @@ nodes: } source: $.productById target: $.products.nodes.b.nodes.product.item.product + batchingGroupId: 8 requirements: - name: __fusion_8_id selectionMap: >- @@ -347,6 +357,7 @@ nodes: } source: $.userById target: $.products.nodes.b.nodes.product.item.product.reviews.edges.node.author + batchingGroupId: 2 requirements: - name: __fusion_10_id selectionMap: >- @@ -354,7 +365,7 @@ nodes: dependencies: - id: 6 - id: 10 - type: Operation + type: OperationBatch schema: e operation: | query Op_123456789101112_10( @@ -365,7 +376,10 @@ nodes: } } source: $.productById - target: $.products.nodes.b.nodes.b + targets: + - $.products.nodes.b.nodes.b + - $.products.nodes.reviews.nodes.b + batchingGroupId: 4 requirements: - name: __fusion_11_id selectionMap: >- @@ -373,7 +387,7 @@ nodes: dependencies: - id: 2 - id: 11 - type: Operation + type: OperationBatch schema: a operation: | query Op_123456789101112_11( @@ -385,63 +399,16 @@ nodes: } } source: $.userById - target: $.products.nodes.b.nodes.author + targets: + - $.products.nodes.b.nodes.author + - $.products.nodes.reviews.nodes.author + batchingGroupId: 1 requirements: - name: __fusion_12_id selectionMap: >- id dependencies: - id: 2 - - id: 12 - type: Operation - schema: e - operation: | - query Op_123456789101112_12( - $__fusion_13_id: ID! - ) { - productById(id: $__fusion_13_id) { - weight - pictureFileName - price - dimension { - length - width - height - } - } - } - source: $.productById - target: $.products.nodes.reviews.nodes.product - requirements: - - name: __fusion_13_id - selectionMap: >- - id - dependencies: - - id: 2 - - id: 13 - type: Operation - schema: b - operation: | - query Op_123456789101112_13( - $__fusion_14_id: ID! - ) { - productByIdAsync(id: $__fusion_14_id) { - quantity - item { - product { - id - } - } - } - } - source: $.productByIdAsync - target: $.products.nodes.reviews.nodes.product - requirements: - - name: __fusion_14_id - selectionMap: >- - id - dependencies: - - id: 2 - id: 14 type: Operation schema: g @@ -456,6 +423,7 @@ nodes: } source: $.productById target: $.products.nodes.reviews.nodes.product + batchingGroupId: 7 requirements: - name: __fusion_15_id selectionMap: >- @@ -470,7 +438,7 @@ nodes: } dependencies: - id: 2 - - id: 12 + - id: 3 - id: 15 type: Operation schema: f @@ -492,12 +460,13 @@ nodes: } source: $.productById target: $.products.nodes.reviews.nodes.product.item.product + batchingGroupId: 6 requirements: - name: __fusion_17_id selectionMap: >- id dependencies: - - id: 13 + - id: 4 - id: 16 type: Operation schema: e @@ -517,12 +486,13 @@ nodes: } source: $.productById target: $.products.nodes.reviews.nodes.product.item.product + batchingGroupId: 5 requirements: - name: __fusion_18_id selectionMap: >- id dependencies: - - id: 13 + - id: 4 - id: 17 type: Operation schema: g @@ -537,6 +507,7 @@ nodes: } source: $.productById target: $.products.nodes.reviews.nodes.product.item.product + batchingGroupId: 8 requirements: - name: __fusion_19_id selectionMap: >- @@ -550,7 +521,7 @@ nodes: height: dimension.height } dependencies: - - id: 13 + - id: 4 - id: 16 - id: 18 type: Operation @@ -565,48 +536,10 @@ nodes: } source: $.userById target: $.products.nodes.reviews.nodes.product.item.product.reviews.edges.node.author + batchingGroupId: 2 requirements: - name: __fusion_21_id selectionMap: >- id dependencies: - id: 15 - - id: 19 - type: Operation - schema: e - operation: | - query Op_123456789101112_19( - $__fusion_22_id: ID! - ) { - productById(id: $__fusion_22_id) { - weight - } - } - source: $.productById - target: $.products.nodes.reviews.nodes.b - requirements: - - name: __fusion_22_id - selectionMap: >- - id - dependencies: - - id: 2 - - id: 20 - type: Operation - schema: a - operation: | - query Op_123456789101112_20( - $__fusion_23_id: ID! - ) { - userById(id: $__fusion_23_id) { - birthdate - username - } - } - source: $.userById - target: $.products.nodes.reviews.nodes.author - requirements: - - name: __fusion_23_id - selectionMap: >- - id - dependencies: - - id: 2 diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6-accounts.graphqls b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6-accounts.graphqls new file mode 100644 index 00000000000..23637dfab66 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6-accounts.graphqls @@ -0,0 +1,23 @@ +schema { + query: Query +} + +type Query { + me: User + user(id: ID!): User @lookup + users: [User!]! +} + +type User { + id: ID! + name: String + username: String + birthday: Int +} + +""" +The @lookup directive is used within a source schema to specify output fields +that can be used by the distributed GraphQL executor to resolve an entity by +a stable key. +""" +directive @lookup on FIELD_DEFINITION \ No newline at end of file diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6-inventory.graphqls b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6-inventory.graphqls new file mode 100644 index 00000000000..255f435f966 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6-inventory.graphqls @@ -0,0 +1,74 @@ +schema { + query: Query +} + +type Product { + shippingEstimate(weight: Long! @require(field: "weight") price: Long! @require(field: "price")): Long! + upc: String! + inStock: Boolean! +} + +type Query { + productByUpc(upc: ID!): Product @lookup @internal +} + +"Defines the possible serialization types for GraphQL scalar values." +enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST +} + +""" +The @internal directive is used in combination with lookup fields and allows you +to declare internal types and fields. Internal types and fields do not appear in +the final client-facing composite schema and do not participate in the standard +schema-merging process. This allows a source schema to define lookup fields for +resolving entities that should not be accessible through the client-facing +composite schema. + + + + + +type User @internal { + id: ID! + name: String! +} + +directive @internal on OBJECT | FIELD_DEFINITION +""" +directive @internal on OBJECT | FIELD_DEFINITION + +""" +The @lookup directive is used within a source schema to specify output fields +that can be used by the distributed GraphQL executor to resolve an entity by +a stable key. +""" +directive @lookup on FIELD_DEFINITION + +""" +The @require directive is used to express data requirements with other source schemas. +Arguments annotated with the @require directive are removed from the composite schema +and the value for these will be resolved by the distributed executor. + + +directive @require(field: FieldSelectionMap!) on ARGUMENT_DEFINITION +""" +directive @require("The field selection map syntax." field: FieldSelectionMap!) on ARGUMENT_DEFINITION + +directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + +scalar FieldSelectionMap + +"The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." +scalar Long @serializeAs(type: INT) \ No newline at end of file diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6-products.graphqls b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6-products.graphqls new file mode 100644 index 00000000000..72b0f304c1b --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6-products.graphqls @@ -0,0 +1,43 @@ +schema { + query: Query +} + +type Product { + upc: String! + name: String! + price: Long! + weight: Long! +} + +type Query { + topProducts(first: Int!): [Product!]! + product(upc: ID!): Product @lookup +} + +"Defines the possible serialization types for GraphQL scalar values." +enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST +} + +""" +The @lookup directive is used within a source schema to specify output fields +that can be used by the distributed GraphQL executor to resolve an entity by +a stable key. +""" +directive @lookup on FIELD_DEFINITION + +directive @serializeAs("The primitive type a scalar is serialized to." type: [ScalarSerializationType!]! "The ECMA-262 regex pattern that the serialized scalar value conforms to." pattern: String) on SCALAR + +"The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." +scalar Long @serializeAs(type: INT) \ No newline at end of file diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6-reviews.graphqls b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6-reviews.graphqls new file mode 100644 index 00000000000..199a5f59681 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6-reviews.graphqls @@ -0,0 +1,56 @@ +schema { + query: Query +} + +type Product { + reviews: [Review!]! + upc: String! +} + +type Query { + product(upc: ID!): Product! @lookup @internal + review(id: ID!): Review @lookup + user(id: ID!): User @lookup @internal +} + +type Review { + id: ID! + author: User + product: Product + body: String! + authorId: String! + productUpc: String! +} + +type User { + id: ID! + reviews: [Review!]! +} + +""" +The @internal directive is used in combination with lookup fields and allows you +to declare internal types and fields. Internal types and fields do not appear in +the final client-facing composite schema and do not participate in the standard +schema-merging process. This allows a source schema to define lookup fields for +resolving entities that should not be accessible through the client-facing +composite schema. + + + + + +type User @internal { + id: ID! + name: String! +} + +directive @internal on OBJECT | FIELD_DEFINITION +""" +directive @internal on OBJECT | FIELD_DEFINITION + +""" +The @lookup directive is used within a source schema to specify output fields +that can be used by the distributed GraphQL executor to resolve an entity by +a stable key. +""" +directive @lookup on FIELD_DEFINITION \ No newline at end of file diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6.graphql b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6.graphql new file mode 100644 index 00000000000..d2b3ba45733 --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Execution.Tests/__resources__/k6.graphql @@ -0,0 +1,69 @@ +query TestQuery { + users { + id + username + name + reviews { + id + body + product { + inStock + name + price + shippingEstimate + upc + weight + reviews { + id + body + author { + id + username + name + reviews { + id + body + product { + inStock + name + price + shippingEstimate + upc + weight + } + } + } + } + } + } + } + topProducts(first: 5) { + inStock + name + price + shippingEstimate + upc + weight + reviews { + id + body + author { + id + username + name + reviews { + id + body + product { + inStock + name + price + shippingEstimate + upc + weight + } + } + } + } + } +} diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/FixedSizeArrayPool.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/FixedSizeArrayPool.cs index d6aa5110957..61ef3ce6448 100644 --- a/src/HotChocolate/Utilities/src/Utilities.Buffers/FixedSizeArrayPool.cs +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/FixedSizeArrayPool.cs @@ -4,20 +4,32 @@ namespace HotChocolate.Buffers; -internal sealed class FixedSizeArrayPool +internal sealed class FixedSizeArrayPool : IDisposable { private readonly int _poolId; private readonly int _arraySize; private readonly int _numberOfArrays; private readonly Bucket _bucket; - public FixedSizeArrayPool(int poolId, int arraySize, int numberOfArrays, bool preAllocate = false) + public FixedSizeArrayPool( + int poolId, + int arraySize, + int[] levels, + TimeSpan trimInterval, + bool preAllocate) { + Debug.Assert( + levels.Length > 0, + "Levels must be a non-empty array."); + Debug.Assert( + trimInterval.TotalSeconds > 10, + "Trim interval should be greater than 10 seconds to avoid excessive trimming."); + _poolId = poolId; _arraySize = arraySize; - _numberOfArrays = numberOfArrays; - _bucket = new Bucket(poolId, arraySize, numberOfArrays, preAllocate); - Log.PoolCreated(poolId, numberOfArrays, (long)numberOfArrays * arraySize); + _numberOfArrays = levels[levels.Length - 1]; + _bucket = new Bucket(poolId, arraySize, levels, trimInterval, preAllocate); + Log.PoolCreated(poolId, _numberOfArrays, (long)_numberOfArrays * arraySize); } public byte[] Rent() @@ -78,23 +90,41 @@ public void Return(byte[] array) } } - private sealed class Bucket + public void Dispose() => _bucket.Dispose(); + + private sealed class Bucket : IDisposable { private readonly int _poolId; private readonly int _bufferLength; private readonly byte[]?[] _buffers; + private readonly int[] _levels; + private readonly Timer _trimTimer; + private int _currentLevel; + private int _inUse; private SpinLock _lock; private int _index; - internal Bucket(int poolId, int bufferLength, int numberOfBuffers, bool preAllocate) + internal Bucket( + int poolId, + int bufferLength, + int[] levels, + TimeSpan trimInterval, + bool preAllocate) { + var numberOfBuffers = levels[levels.Length - 1]; + _poolId = poolId; _bufferLength = bufferLength; _buffers = new byte[numberOfBuffers][]; + _levels = levels; + + _currentLevel = _levels.Length - 1; if (preAllocate) { - for (var i = 0; i < _buffers.Length; i++) + // only pre-allocate up to the stable level (first entry in _levels). + var stableLevel = levels[0]; + for (var i = 0; i < stableLevel; i++) { _buffers[i] = new byte[_bufferLength]; } @@ -102,12 +132,17 @@ internal Bucket(int poolId, int bufferLength, int numberOfBuffers, bool preAlloc _lock = new SpinLock(Debugger.IsAttached); _index = 0; + _inUse = 0; + + _trimTimer = new Timer(static b => ((Bucket)b!).Trim(), this, trimInterval, trimInterval); } - internal int InUse => _index; + internal int InUse => _inUse; internal byte[]? Rent() { + Interlocked.Increment(ref _inUse); + var buffers = _buffers; byte[]? buffer = null; @@ -156,6 +191,8 @@ internal Bucket(int poolId, int bufferLength, int numberOfBuffers, bool preAlloc internal bool Return(byte[] array) { + Interlocked.Decrement(ref _inUse); + // if the returned array has not the expected size we will reject it without throwing an error. if (array.Length != _bufferLength) { @@ -185,5 +222,76 @@ internal bool Return(byte[] array) return returned; } + + // called from the TrimCallback timer once per minute. + // if the pool is not under pressure we step the current level down one + // notch and null out the buffer slots between the new and old level so + // those byte[] arrays become eligible for collection. + private void Trim() + { + var currentLevel = _currentLevel; + + // nothing to trim if we are already at the stable level. + if (currentLevel == 0) + { + return; + } + + var previousLevel = currentLevel - 1; + var previousLimit = _levels[previousLevel]; + + // if outstanding buffers exceed the target level the pool is still + // under pressure — skip trimming. + if (_inUse > previousLimit) + { + return; + } + var trimmed = 0; + + var lockTaken = false; + + try + { + var currentLimit = _levels[currentLevel]; + + _lock.Enter(ref lockTaken); + + // null out slots between the new limit and the old limit. + // only null slots that are beyond _index (i.e. not currently + // rented out — those slots are already null). + for (var i = previousLimit; i < currentLimit; i++) + { + if (_buffers[i] != null) + { + _buffers[i] = null; + trimmed++; + } + } + + // if _index is beyond the new limit we need to pull it back + // and release those buffers too. + if (_index > previousLimit) + { + _index = previousLimit; + } + } + finally + { + if (lockTaken) + { + _lock.Exit(false); + } + } + + _currentLevel = previousLevel; + + var log = Log; + if (log.IsEnabled()) + { + log.PoolTrimmed(_poolId, trimmed, previousLimit, _inUse); + } + } + + public void Dispose() => _trimTimer.Dispose(); } } diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/FixedSizeArrayPoolEventSource.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/FixedSizeArrayPoolEventSource.cs index d219a9e4e2b..575f1f3d8fc 100644 --- a/src/HotChocolate/Utilities/src/Utilities.Buffers/FixedSizeArrayPoolEventSource.cs +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/FixedSizeArrayPoolEventSource.cs @@ -80,4 +80,16 @@ public void BufferAllocated(int bufferId, int bufferSize, int poolId) WriteEvent(6, bufferId, bufferSize, poolId); } } + + [Event( + eventId: 7, + Level = EventLevel.Informational, + Message = "Pool trimmed (PoolId={0}, Trimmed={1}, Remaining={2}, InUse={3})")] + public void PoolTrimmed(int poolId, int trimmed, int remaining, int inUse) + { + if (IsEnabled()) + { + WriteEvent(7, poolId, trimmed, remaining, inUse); + } + } } diff --git a/src/HotChocolate/Utilities/src/Utilities.Buffers/JsonMemory.cs b/src/HotChocolate/Utilities/src/Utilities.Buffers/JsonMemory.cs index 4569611345c..af1e01d783a 100644 --- a/src/HotChocolate/Utilities/src/Utilities.Buffers/JsonMemory.cs +++ b/src/HotChocolate/Utilities/src/Utilities.Buffers/JsonMemory.cs @@ -13,7 +13,13 @@ internal static class JsonMemory { public const int BufferSize = 1 << 17; - private static FixedSizeArrayPool s_pool = new(FixedSizeArrayPoolKinds.JsonMemory, BufferSize, 128); + private static FixedSizeArrayPool s_pool = + new FixedSizeArrayPool( + FixedSizeArrayPoolKinds.JsonMemory, + arraySize: BufferSize, + [128, 768, 3072], + trimInterval: TimeSpan.FromMinutes(5), + preAllocate: true); private static readonly ArrayPool s_chunkPool = ArrayPool.Shared; public static void Reconfigure(Func factory) @@ -27,7 +33,10 @@ public static void Reconfigure(Func factory) } #endif - s_pool = factory() ?? throw new InvalidOperationException("The factory must create a valid pool."); + var oldPool = Interlocked.Exchange( + ref s_pool, + factory() ?? throw new InvalidOperationException("The factory must create a valid pool.")); + oldPool.Dispose(); Log.ReconfiguredPool(); }