private async Task ExecuteOperation(GraphQueryExecutionContext context) { // create a cancelation sourc irrespective of the required timeout for exeucting this operation // this allows for indication of why a task was canceled (timeout or other user driven reason) // vs just "it was canceled" which allows for tighter error messages in the response. var operation = context.QueryOperation; var fieldInvocations = new List <FieldPipelineInvocation>(); // Convert the supplied variable values to usable objects of the type expression // of the chosen operation var variableResolver = new ResolvedVariableGenerator(_schema, operation); var variableData = variableResolver.Resolve(context.Request.VariableData); var cancelSource = new CancellationTokenSource(); try { // begin a field execution pipeline for each top level field foreach (var invocationContext in operation.FieldContexts) { var path = new SourcePath(); path.AddFieldName(invocationContext.Name); object dataSourceValue; // fetch the source data value to use for the field invocation // attempt to retrieve from the master context if it was supplied by the pipeline // invoker, otherwise generate a root source if (!context.DefaultFieldSources.TryRetrieveSource(invocationContext.Field, out dataSourceValue)) { dataSourceValue = this.GenerateRootSourceData(operation.OperationType); } var topLevelDataItem = new GraphDataItem(invocationContext, dataSourceValue, path); var sourceData = new GraphFieldDataSource(dataSourceValue, path, topLevelDataItem); var fieldRequest = new GraphFieldRequest( invocationContext, sourceData, new SourceOrigin(invocationContext.Origin.Location, path), context.Items); var fieldContext = new GraphFieldExecutionContext( context, fieldRequest, variableData, context.DefaultFieldSources); var fieldTask = _fieldExecutionPipeline.InvokeAsync(fieldContext, cancelSource.Token); var pipelineInvocation = new FieldPipelineInvocation() { Task = fieldTask, DataItem = topLevelDataItem, FieldContext = fieldContext, }; fieldInvocations.Add(pipelineInvocation); // top level mutation operatons must be executed in sequential order // https://graphql.github.io/graphql-spec/June2018/#sec-Mutation if (_awaitEachTask || operation.OperationType == GraphCollection.Mutation) { await fieldTask.ConfigureAwait(false); } } // await all the outstanding tasks or a configured timeout var fieldPipelineTasksWrapper = Task.WhenAll(fieldInvocations.Select(x => x.Task)); var timeOutTask = Task.Delay(_timeoutMs, cancelSource.Token); var completedTask = await Task.WhenAny(fieldPipelineTasksWrapper, timeOutTask).ConfigureAwait(false); var isTimedOut = completedTask == timeOutTask; var cancelationWasRequested = cancelSource.IsCancellationRequested; if (!isTimedOut) { // Field resolutions completed within the timeout period. // Consider that the task may have faulted or been canceled causing them to complete incorrectly. // "re-await" so that any exceptions/cancellation are rethrown correctly. // and not aggregated under the `WhenAll/WhenAny` task from above // https://stackoverflow.com/questions/4238345/asynchronously-wait-for-taskt-to-complete-with-timeout foreach (var invocation in fieldInvocations) { await invocation.Task.ConfigureAwait(false); // load the reslts of each field (in order) to the context // for further processing context.FieldResults.Add(invocation.DataItem); context.Messages.AddRange(invocation.FieldContext.Messages); } } else { // when the timeout finishes first, process the cancel token in case any outstanding tasks are running // helps in cases where the timeout finished first but any of the field resolutions are perhaps stuck open // instruct all outstanding tasks to clean them selves up at the earlest possible point if (!cancelationWasRequested) { cancelSource.Cancel(); } } if (cancelationWasRequested) { context.Messages.Critical("The execution was canceled prior to completion of the requested query.", Constants.ErrorCodes.OPERATION_CANCELED); } else if (isTimedOut) { context.Messages.Critical($"The execution timed out prior to completion of the requested query. (Total Time: {_timeoutMs}ms)", Constants.ErrorCodes.OPERATION_CANCELED); } } finally { cancelSource.Dispose(); } }
/// <summary> /// Using the child context being invoked, this method creates the execution contexts in a manner /// that is expected by the invocation context be that 1 per each item, or 1 for a collective set of items being batched. /// </summary> /// <param name="context">The context.</param> /// <param name="childInvocationContext">The child invocation context.</param> /// <param name="sourceItemsToInclude">The source items to include.</param> /// <returns>IEnumerable<GraphFieldExecutionContext>.</returns> private IEnumerable <GraphFieldExecutionContext> CreateChildExecutionContexts( GraphFieldExecutionContext context, IGraphFieldInvocationContext childInvocationContext, IEnumerable <GraphDataItem> sourceItemsToInclude) { if (childInvocationContext.Field.Mode == FieldResolutionMode.PerSourceItem) { foreach (var sourceItem in sourceItemsToInclude) { var child = sourceItem.AddChildField(childInvocationContext); var dataSource = new GraphFieldDataSource(sourceItem.ResultData, child.Origin.Path, child); var request = new GraphFieldRequest(childInvocationContext, dataSource, child.Origin, context.Request.Items); yield return(new GraphFieldExecutionContext( context, request, context.VariableData, context.DefaultFieldSources)); } } else if (childInvocationContext.Field.Mode == FieldResolutionMode.Batch) { // remove any potential indexers from the path to this batch operation // in general this will be acted on a collection of items, attempt to remove // the first found instance of an indexer in the chain to indicate the path to the batch // // items may be declared as: Top.Parent[0].BatchField, Top.Parent[1].BatchField // alter the canonical path to be: Top.Parent.BatchField var fieldPath = sourceItemsToInclude.First().Origin.Path.Clone(); while (fieldPath.IsIndexedItem) { fieldPath = fieldPath.MakeParent(); } fieldPath.AddFieldName(childInvocationContext.Field.Name); var batchOrigin = new SourceOrigin(context.Request.Origin.Location, fieldPath); // create a list to house the raw source data being passed for the batch // this is the IEnumerable<T> required as an input to any batch resolver var sourceArgumentType = childInvocationContext.Field.Arguments.SourceDataArgument?.ObjectType ?? typeof(object); var sourceListType = typeof(List <>).MakeGenericType(sourceArgumentType); var sourceDataList = InstanceFactory.CreateInstance(sourceListType) as IList; // create a list of all the GraphDataItems representing the field // being resolved per input item var sourceItemList = new List <GraphDataItem>(); foreach (var item in sourceItemsToInclude) { var childField = item.AddChildField(childInvocationContext); sourceDataList.Add(item.ResultData); sourceItemList.Add(childField); } var dataSource = new GraphFieldDataSource( sourceDataList, fieldPath, sourceItemList); var request = new GraphFieldRequest(childInvocationContext, dataSource, batchOrigin, context.Request.Items); yield return(new GraphFieldExecutionContext( context, request, context.VariableData, context.DefaultFieldSources)); } else { throw new ArgumentOutOfRangeException( nameof(childInvocationContext.Field.Mode), $"The execution mode for field '{childInvocationContext.Field.Route.Path}' cannot be processed " + $"by {nameof(ProcessChildFieldsMiddleware<TSchema>)}. (Mode: {childInvocationContext.Field.Mode.ToString()})"); } }