From f59a9f143c3e59c983f20ef4f1abb4494af4ef82 Mon Sep 17 00:00:00 2001 From: MohamedSabthar Date: Tue, 15 Oct 2024 13:15:56 +0530 Subject: [PATCH] Introduce Parallel Execution for the GraphQL Resolvers --- ballerina/context.bal | 261 ++++++++---------- ballerina/engine.bal | 30 +- ballerina/engine_utils.bal | 2 +- ballerina/executor_visitor.bal | 139 +++++++--- ballerina/field.bal | 47 +++- ballerina/modules/parser/operation_node.bal | 6 +- ballerina/response_generator.bal | 167 +++++++---- ballerina/tests/utils.bal | 2 +- ballerina/value_tree_builder.bal | 14 +- changelog.md | 5 + .../graphql/runtime/engine/Context.java | 209 ++++++++++++++ .../runtime/engine/ExecutorVisitor.java | 65 +++++ .../graphql/runtime/engine/ListenerUtils.java | 4 +- 13 files changed, 681 insertions(+), 270 deletions(-) create mode 100644 native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/Context.java create mode 100644 native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ExecutorVisitor.java diff --git a/ballerina/context.bal b/ballerina/context.bal index 417282491..617f0c91c 100644 --- a/ballerina/context.bal +++ b/ballerina/context.bal @@ -22,30 +22,24 @@ import ballerina/lang.value; # The GraphQL context object used to pass the meta information between resolvers. public isolated class Context { - private final map attributes = {}; private final ErrorDetail[] errors = []; private Engine? engine = (); - private int nextInterceptor = 0; private boolean hasFileInfo = false; // This field value changed by setFileInfo method - private map idDataLoaderMap = {}; // Provides mapping between user defined id and DataLoader - private map uuidPlaceholderMap = {}; - private Placeholder[] unResolvedPlaceholders = []; - private boolean containPlaceholders = false; - private int unResolvedPlaceholderCount = 0; // Tracks the number of Placeholders needs to be resolved - private int unResolvedPlaceholderNodeCount = 0; // Tracks the number of nodes to be replaced in the value tree + + public isolated function init() { + self.initializeContext(); + } + + isolated function initializeContext() = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; # Sets a given value for a given key in the GraphQL context. # # + key - The key for the value to be set # + value - Value to be set public isolated function set(string 'key, value:Cloneable|isolated object {} value) { - lock { - if value is value:Cloneable { - self.attributes['key] = value.clone(); - } else { - self.attributes['key] = value; - } - } + self.setAttribute('key, value is value:Cloneable ? value.clone() : value); } # Retrieves a value using the given key from the GraphQL context. @@ -53,17 +47,11 @@ public isolated class Context { # + key - The key corresponding to the required value # + return - The value if the key is present in the context, a `graphql:Error` otherwise public isolated function get(string 'key) returns value:Cloneable|isolated object {}|Error { - lock { - if self.attributes.hasKey('key) { - value:Cloneable|isolated object {} value = self.attributes.get('key); - if value is value:Cloneable { - return value.clone(); - } else { - return value; - } - } - return error Error(string`Attribute with the key "${'key}" not found in the context`); + value:Cloneable|isolated object {}? value = self.getAttribute('key); + if value is () { + return error Error(string `Attribute with the key "${'key}" not found in the context`); } + return value is value:Cloneable ? value.clone() : value; } # Removes a value using the given key from the GraphQL context. @@ -71,38 +59,28 @@ public isolated class Context { # + key - The key corresponding to the value to be removed # + return - The value if the key is present in the context, a `graphql:Error` otherwise public isolated function remove(string 'key) returns value:Cloneable|isolated object {}|Error { - lock { - if self.attributes.hasKey('key) { - value:Cloneable|isolated object {} value = self.attributes.remove('key); - if value is value:Cloneable { - return value.clone(); - } else { - return value; - } - } - return error Error(string`Attribute with the key "${'key}" not found in the context`); + value:Cloneable|isolated object {}? value = self.removeAttribute('key); + if value is () { + return error Error(string `Attribute with the key "${'key}" not found in the context`); } + return value is value:Cloneable ? value.clone() : value; } # Register a given DataLoader instance for a given key in the GraphQL context. # # + key - The key for the DataLoader to be registered # + dataloader - The DataLoader instance to be registered - public isolated function registerDataLoader(string key, dataloader:DataLoader dataloader) { - lock { - self.idDataLoaderMap[key] = dataloader; - } - } + public isolated function registerDataLoader(string key, dataloader:DataLoader dataloader) = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; # Retrieves a DataLoader instance using the given key from the GraphQL context. # # + key - The key corresponding to the required DataLoader instance # + return - The DataLoader instance if the key is present in the context otherwise panics - public isolated function getDataLoader(string key) returns dataloader:DataLoader { - lock { - return self.idDataLoaderMap.get(key); - } - } + public isolated function getDataLoader(string key) returns dataloader:DataLoader = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; # Remove cache entries related to the given path. # @@ -174,124 +152,127 @@ public isolated class Context { } } - isolated function getNextInterceptor(Field 'field) returns (readonly & Interceptor)? { - Engine? engine = self.getEngine(); - if engine is Engine { - (readonly & Interceptor)[] interceptors = engine.getInterceptors(); - if interceptors.length() > self.getInterceptorCount() { - (readonly & Interceptor) next = interceptors[self.getInterceptorCount()]; - if !isGlobalInterceptor(next) && 'field.getPath().length() > 1 { - self.increaseInterceptorCount(); - return self.getNextInterceptor('field); - } - self.increaseInterceptorCount(); - return next; - } - int nextFieldInterceptor = self.getInterceptorCount() - engine.getInterceptors().length(); - if 'field.getFieldInterceptors().length() > nextFieldInterceptor { - readonly & Interceptor next = 'field.getFieldInterceptors()[nextFieldInterceptor]; - self.increaseInterceptorCount(); - return next; - } - } - self.resetInterceptorCount(); - return; - } - - isolated function resetInterceptorCount() { - lock { - self.nextInterceptor = 0; - } - } - - isolated function getInterceptorCount() returns int { - lock { - return self.nextInterceptor; - } - } - - isolated function increaseInterceptorCount() { - lock { - self.nextInterceptor += 1; - } - } - isolated function resetErrors() { lock { self.errors.removeAll(); } } - isolated function addUnresolvedPlaceholder(string uuid, Placeholder placeholder) { - lock { - self.containPlaceholders = true; - self.uuidPlaceholderMap[uuid] = placeholder; - self.unResolvedPlaceholders.push(placeholder); - self.unResolvedPlaceholderCount += 1; - self.unResolvedPlaceholderNodeCount += 1; - } - } - isolated function resolvePlaceholders() { - lock { - string[] nonDispatchedDataLoaderIds = self.idDataLoaderMap.keys(); - Placeholder[] unResolvedPlaceholders = self.unResolvedPlaceholders; - self.unResolvedPlaceholders = []; - foreach string dataLoaderId in nonDispatchedDataLoaderIds { - self.idDataLoaderMap.get(dataLoaderId).dispatch(); - } - foreach Placeholder placeholder in unResolvedPlaceholders { - Engine? engine = self.getEngine(); - if engine is () { - continue; - } - anydata resolvedValue = engine.resolve(self, 'placeholder.getField(), false); - placeholder.setValue(resolvedValue); - self.unResolvedPlaceholderCount -= 1; + self.dispatchDataloaders(); + Placeholder[] unResolvedPlaceholders = self.getUnresolvedPlaceholders(); + self.removeAllUnresolvedPlaceholders(); + [Placeholder, future][] placeholderValues = []; + foreach Placeholder placeholder in unResolvedPlaceholders { + Engine? engine = self.getEngine(); + if engine is () { + continue; } + future resolvedValue = start engine.resolve(self, placeholder.getField(), false); + placeholderValues.push([placeholder, resolvedValue]); } - } - - isolated function getPlaceholderValue(string uuid) returns anydata { - lock { - return self.uuidPlaceholderMap.remove(uuid).getValue(); - } - } - - isolated function getUnresolvedPlaceholderCount() returns int { - lock { - return self.unResolvedPlaceholderCount; + foreach [Placeholder, future] [placeholder, 'future] in placeholderValues { + anydata|error resolvedValue = wait 'future; + if resolvedValue is error { + self.addError({message: resolvedValue.message()}); + self.decrementUnresolvedPlaceholderCount(); + continue; + } + placeholder.setValue(resolvedValue); + self.decrementUnresolvedPlaceholderCount(); } } - isolated function getUnresolvedPlaceholderNodeCount() returns int { - lock { - return self.unResolvedPlaceholderNodeCount; + isolated function dispatchDataloaders() { + string[] nonDispatchedDataLoaderIds = self.getDataLoaderIds(); + future<()>[] dataloaders = []; + foreach string dataLoaderId in nonDispatchedDataLoaderIds { + dataloader:DataLoader dataloader = self.getDataLoader(dataLoaderId); + future<()> 'future = start dataloader.dispatch(); + dataloaders.push('future); } - } - - isolated function decrementUnresolvedPlaceholderNodeCount() { - lock { - self.unResolvedPlaceholderNodeCount-=1; + foreach future<()> 'future in dataloaders { + error? err = wait 'future; + if err is error { + ErrorDetail errorDetail = { + message: err.message() + }; + self.addError(errorDetail); + continue; + } } } - isolated function hasPlaceholders() returns boolean { - lock { - return self.containPlaceholders; - } + isolated function getPlaceholderValue(string uuid) returns anydata { + Placeholder placeholder = self.getPlaceholder(uuid); + return placeholder.getValue(); } isolated function clearDataLoadersCachesAndPlaceholders() { // This function is called at the end of each subscription loop execution to prevent using old values // from DataLoader caches in the next iteration and to avoid filling up the idPlaceholderMap. - lock { - self.idDataLoaderMap.forEach(dataloader => dataloader.clearAll()); - self.unResolvedPlaceholders.removeAll(); - self.uuidPlaceholderMap.removeAll(); - self.containPlaceholders = false; + string[] nonDispatchedDataLoaderIds = self.getDataLoaderIds(); + foreach string dataLoaderId in nonDispatchedDataLoaderIds { + self.getDataLoader(dataLoaderId).clearAll(); } + self.clearPlaceholders(); } + + isolated function setAttribute(string uuid, value:Cloneable|isolated object {} value) = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; + + isolated function getAttribute(string uuid) returns value:Cloneable|isolated object {}? = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; + + isolated function removeAttribute(string uuid) returns value:Cloneable|isolated object {}? = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; + + isolated function getPlaceholder(string uuid) returns Placeholder = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; + + isolated function clearPlaceholders() = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; + + isolated function getUnresolvedPlaceholders() returns Placeholder[] = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; + + isolated function getDataLoaderIds() returns string[] = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; + + isolated function getUnresolvedPlaceholderCount() returns int = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; + + isolated function getUnresolvedPlaceholderNodeCount() returns int = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; + + isolated function decrementUnresolvedPlaceholderNodeCount() = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; + + isolated function hasPlaceholders() returns boolean = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; + + isolated function addUnresolvedPlaceholder(string uuid, Placeholder placeholder) = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; + + isolated function removeAllUnresolvedPlaceholders() = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; + + isolated function decrementUnresolvedPlaceholderCount() = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.Context" + } external; } isolated function initDefaultContext(http:RequestContext requestContext, http:Request request) returns Context|error { diff --git a/ballerina/engine.bal b/ballerina/engine.bal index 33659c46f..cf0dded9e 100644 --- a/ballerina/engine.bal +++ b/ballerina/engine.bal @@ -286,10 +286,10 @@ isolated class Engine { ?: getDefaultPrefetchMethodName(fieldNode.getName()); if self.hasPrefetchMethod(serviceObject, prefetchMethodName) { addTracingInfomation({ - context, - serviceName: prefetchMethodName, - operationType: 'field.getOperationType() - }); + context, + serviceName: prefetchMethodName, + operationType: 'field.getOperationType() + }); anydata result = self.getResultFromPrefetchMethodExecution(context, 'field, serviceObject, prefetchMethodName); stopTracing(context); return result; @@ -297,7 +297,7 @@ isolated class Engine { } } - (readonly & Interceptor)? interceptor = context.getNextInterceptor('field); + (readonly & Interceptor)? interceptor = 'field.getNextInterceptor(self); __Type fieldType = 'field.getFieldType(); ResponseGenerator responseGenerator = new (self, context, fieldType, 'field.getPath().clone(), 'field.getCacheConfig(), 'field.getParentArgHashes() @@ -306,10 +306,10 @@ isolated class Engine { if interceptor is readonly & Interceptor { string interceptorName = self.getInterceptorName(interceptor); addTracingInfomation({ - context, - serviceName: interceptorName, - operationType: 'field.getOperationType() - }); + context, + serviceName: interceptorName, + operationType: 'field.getOperationType() + }); any|error result = self.executeInterceptor(interceptor, 'field, context); anydata response = check validateInterceptorReturnValue(fieldType, result, interceptorName); stopTracing(context); @@ -335,10 +335,10 @@ isolated class Engine { } } else { addTracingInfomation({ - context, - serviceName: 'field.getName(), - operationType - }); + context, + serviceName: 'field.getName(), + operationType + }); addFieldMetric('field); fieldValue = check self.getFieldValue(context, 'field, responseGenerator); } @@ -414,14 +414,12 @@ isolated class Engine { isolated function getHierarchicalResult(Context context, Field 'field, parser:FieldNode fieldNode, map result) { string[] resourcePath = 'field.getResourcePath(); - (string|int)[] path = 'field.getPath().clone(); - path.push(fieldNode.getName()); + readonly & (string|int)[] path = [...'field.getPath(), fieldNode.getName()]; __Type parentType = 'field.getFieldType(); __Type fieldType = getFieldTypeFromParentType(parentType, self.schema.types, fieldNode); Field selectionField = new (fieldNode, fieldType, parentType, 'field.getServiceObject(), path = path, resourcePath = resourcePath ); - context.resetInterceptorCount(); anydata fieldValue = self.resolve(context, selectionField); result[fieldNode.getAlias()] = fieldValue is ErrorDetail ? () : fieldValue; _ = resourcePath.pop(); diff --git a/ballerina/engine_utils.bal b/ballerina/engine_utils.bal index 2e569f4a9..39749f530 100644 --- a/ballerina/engine_utils.bal +++ b/ballerina/engine_utils.bal @@ -86,7 +86,7 @@ isolated function isValidReturnType(__Type 'type, anydata value) returns boolean isolated function getFieldObject(parser:FieldNode fieldNode, parser:RootOperationType operationType, __Schema schema, Engine engine, any|error fieldValue = ()) returns Field { - (string|int)[] path = [fieldNode.getAlias()]; + readonly & (string|int)[] path = [fieldNode.getAlias()]; string operationTypeName = getOperationTypeNameFromOperationType(operationType); __Type parentType = <__Type>getTypeFromTypeArray(schema.types, operationTypeName); __Type fieldType = getFieldTypeFromParentType(parentType, schema.types, fieldNode); diff --git a/ballerina/executor_visitor.bal b/ballerina/executor_visitor.bal index 61f67dd0f..4a18dfbce 100644 --- a/ballerina/executor_visitor.bal +++ b/ballerina/executor_visitor.bal @@ -15,6 +15,7 @@ // under the License. import graphql.parser; + import ballerina/jballerina.java; isolated class ExecutorVisitor { @@ -22,17 +23,16 @@ isolated class ExecutorVisitor { private final readonly & __Schema schema; private final Engine engine; // This field needed to be accessed from the native code - private Data data; - private Context context; + private final Context context; private any|error result; // The value of this field is set using setResult method isolated function init(Engine engine, readonly & __Schema schema, Context context, any|error result = ()) { self.engine = engine; self.schema = schema; self.context = context; - self.data = {}; self.result = (); self.setResult(result); + self.initializeDataMap(); } public isolated function visitDocument(parser:DocumentNode documentNode, anydata data = ()) {} @@ -42,6 +42,14 @@ isolated class ExecutorVisitor { if operationNode.getName() != parser:ANONYMOUS_OPERATION { path.push(operationNode.getName()); } + service object {} serviceObject; + lock { + serviceObject = self.engine.getService(); + } + if operationNode.getKind() != parser:OPERATION_MUTATION && serviceObject is isolated service object {} { + map dataMap = {[OPERATION_TYPE] : operationNode.getKind(), [PATH] : path}; + return self.visitSelectionsParallelly(operationNode, dataMap.cloneReadOnly()); + } foreach parser:SelectionNode selection in operationNode.getSelections() { if selection is parser:FieldNode { path.push(selection.getName()); @@ -54,31 +62,26 @@ isolated class ExecutorVisitor { public isolated function visitField(parser:FieldNode fieldNode, anydata data = ()) { parser:RootOperationType operationType = self.getOperationTypeFromData(data); boolean isIntrospection = true; - lock { - if fieldNode.getName() == SCHEMA_FIELD { - IntrospectionExecutor introspectionExecutor = new(self.schema); - self.data[fieldNode.getAlias()] = introspectionExecutor.getSchemaIntrospection(fieldNode); - } else if fieldNode.getName() == TYPE_FIELD { - IntrospectionExecutor introspectionExecutor = new(self.schema); - self.data[fieldNode.getAlias()] = introspectionExecutor.getTypeIntrospection(fieldNode); - } else if fieldNode.getName() == TYPE_NAME_FIELD { - if operationType == parser:OPERATION_QUERY { - self.data[fieldNode.getAlias()] = QUERY_TYPE_NAME; - } else if operationType == parser:OPERATION_MUTATION { - self.data[fieldNode.getAlias()] = MUTATION_TYPE_NAME; - } else { - self.data[fieldNode.getAlias()] = SUBSCRIPTION_TYPE_NAME; - } + if fieldNode.getName() == SCHEMA_FIELD { + IntrospectionExecutor introspectionExecutor = new (self.schema); + self.addData(fieldNode.getAlias(), introspectionExecutor.getSchemaIntrospection(fieldNode)); + } else if fieldNode.getName() == TYPE_FIELD { + IntrospectionExecutor introspectionExecutor = new (self.schema); + self.addData(fieldNode.getAlias(), introspectionExecutor.getTypeIntrospection(fieldNode)); + } else if fieldNode.getName() == TYPE_NAME_FIELD { + if operationType == parser:OPERATION_QUERY { + self.addData(fieldNode.getAlias(), QUERY_TYPE_NAME); + } else if operationType == parser:OPERATION_MUTATION { + self.addData(fieldNode.getAlias(), MUTATION_TYPE_NAME); } else { - isIntrospection = false; + self.addData(fieldNode.getAlias(), SUBSCRIPTION_TYPE_NAME); } + } else { + isIntrospection = false; } - isolated function (parser:FieldNode, parser:RootOperationType) execute; - lock { - execute = self.execute; - } + if !isIntrospection { - execute(fieldNode, operationType); + self.execute(fieldNode, operationType); } } @@ -87,46 +90,86 @@ isolated class ExecutorVisitor { public isolated function visitFragment(parser:FragmentNode fragmentNode, anydata data = ()) { parser:RootOperationType operationType = self.getOperationTypeFromData(data); string[] path = self.getSelectionPathFromData(data); + if operationType != parser:OPERATION_MUTATION { + map dataMap = {[OPERATION_TYPE] : operationType, [PATH] : path}; + return self.visitSelectionsParallelly(fragmentNode, dataMap.cloneReadOnly()); + } foreach parser:SelectionNode selection in fragmentNode.getSelections() { - string[] clonedPath = path.clone(); if selection is parser:FieldNode { - clonedPath.push(selection.getName()); + path.push(selection.getName()); } - map dataMap = {[OPERATION_TYPE] : operationType, [PATH] : clonedPath}; + map dataMap = {[OPERATION_TYPE] : operationType, [PATH] : path}; selection.accept(self, dataMap); } } - public isolated function visitDirective(parser:DirectiveNode directiveNode, anydata data = ()) {} + public isolated function visitDirective(parser:DirectiveNode directiveNode, anydata data = ()) { + } - public isolated function visitVariable(parser:VariableNode variableNode, anydata data = ()) {} + public isolated function visitVariable(parser:VariableNode variableNode, anydata data = ()) { + } isolated function execute(parser:FieldNode fieldNode, parser:RootOperationType operationType) { any|error result; - __Schema schema; + __Schema schema = self.schema; Engine engine; Context context; lock { result = self.getResult(); - schema = self.schema; engine = self.engine; context = self.context; } Field 'field = getFieldObject(fieldNode, operationType, schema, engine, result); - context.resetInterceptorCount(); anydata resolvedResult = engine.resolve(context, 'field); - lock { - self.data[fieldNode.getAlias()] = resolvedResult is ErrorDetail ? () : resolvedResult.cloneReadOnly(); - } + self.addData(fieldNode.getAlias(), resolvedResult is ErrorDetail ? () : resolvedResult); } isolated function getOutput() returns OutputObject { + Context context; lock { - if !self.context.hasPlaceholders() { - // Avoid rebuilding the value tree if there are no place holders - return getOutputObject(self.data.clone(), self.context.getErrors().clone()); + context = self.context; + } + Data data = self.getDataMap(); + ErrorDetail[] errors = context.getErrors(); + if !self.context.hasPlaceholders() { + // Avoid rebuilding the value tree if there are no place holders + return getOutputObject(data, errors); + } + ValueTreeBuilder valueTreeBuilder = new (); + data = valueTreeBuilder.build(context, data); + errors = context.getErrors(); + return getOutputObject(data, errors); + } + + private isolated function visitSelectionsParallelly(parser:SelectionParentNode selectionParentNode, + readonly & anydata data = ()) { + parser:RootOperationType operationType = self.getOperationTypeFromData(data); + [parser:SelectionNode, future<()>][] selectionFutures = []; + string[] path = self.getSelectionPathFromData(data); + foreach parser:SelectionNode selection in selectionParentNode.getSelections() { + if selection is parser:FieldNode { + path.push(selection.getName()); + } + map dataMap = {[OPERATION_TYPE] : operationType, [PATH] : path}; + future<()> 'future = start selection.accept(self, dataMap.cloneReadOnly()); + selectionFutures.push([selection, 'future]); + } + foreach [parser:SelectionNode, future<()>] [selection, 'future] in selectionFutures { + error? err = wait 'future; + if err is () { + continue; + } + if selection is parser:FieldNode { + path.push(selection.getName()); + self.addData(selection.getAlias(), ()); + ErrorDetail errorDetail = { + message: err.message(), + locations: [selection.getLocation()], + path: path + }; + lock { + self.context.addError(errorDetail); + } } - ValueTreeBuilder valueTreeBuilder = new (self.context, self.data); - return getOutputObject(valueTreeBuilder.build(), self.context.getErrors().clone()); } } @@ -141,11 +184,23 @@ isolated class ExecutorVisitor { return dataMap[OPERATION_TYPE]; } - private isolated function setResult(any|error result) = @java:Method { + private isolated function setResult(any|error result) = @java:Method { 'class: "io.ballerina.stdlib.graphql.runtime.engine.EngineUtils" } external; - private isolated function getResult() returns any|error = @java:Method { + isolated function initializeDataMap() = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.ExecutorVisitor" + } external; + + private isolated function addData(string key, anydata value) = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.ExecutorVisitor" + } external; + + private isolated function getResult() returns any|error = @java:Method { 'class: "io.ballerina.stdlib.graphql.runtime.engine.EngineUtils" } external; + + private isolated function getDataMap() returns Data = @java:Method { + 'class: "io.ballerina.stdlib.graphql.runtime.engine.ExecutorVisitor" + } external; } diff --git a/ballerina/field.bal b/ballerina/field.bal index eb56e92d0..1f1dbb9ff 100644 --- a/ballerina/field.bal +++ b/ballerina/field.bal @@ -24,7 +24,7 @@ public class Field { private final any|error fieldValue; private final __Type fieldType; private final __Type parentType; - private (string|int)[] path; + private final readonly & (string|int)[] path; private string[] resourcePath; private readonly & Interceptor[] fieldInterceptors; private final ServerCacheConfig? cacheConfig; @@ -33,11 +33,13 @@ public class Field { private final decimal cacheMaxAge; private boolean hasRequestedNullableFields; private final boolean alreadyCached; + private int nextInterceptor = 0; isolated function init(parser:FieldNode internalNode, __Type fieldType, __Type parentType, - service object {}? serviceObject = (), (string|int)[] path = [], + service object {}? serviceObject = (), readonly & (string|int)[] path = [], parser:RootOperationType operationType = parser:OPERATION_QUERY, string[] resourcePath = [], - any|error fieldValue = (), ServerCacheConfig? cacheConfig = (), readonly & string[] parentArgHashes = [], boolean isAlreadyCached = false) { + any|error fieldValue = (), ServerCacheConfig? cacheConfig = (), readonly & string[] parentArgHashes = [], + boolean isAlreadyCached = false) { self.internalNode = internalNode; self.serviceObject = serviceObject; self.fieldType = fieldType; @@ -86,7 +88,7 @@ public class Field { # Returns the current path of the field. If the field returns an array, the path will include the index of the # element. # + return - The path of the field - public isolated function getPath() returns (string|int)[] { + public isolated function getPath() returns readonly & (string|int)[] { return self.path; } @@ -147,7 +149,7 @@ public class Field { } isolated function getFieldObjects(parser:SelectionNode selectionNode, __Type 'type) returns Field[] { - string[] currentPath = self.path.clone().'map((item) => item is int ? "@" : item); + string[] currentPath = self.path.'map((item) => item is int ? "@" : item); string[] unwrappedPath = getUnwrappedPath('type); __Type parentType = getOfType('type); @@ -242,4 +244,39 @@ public class Field { } return requestedNullableFields.sort(); } + + isolated function getNextInterceptor(Engine? engine) returns (readonly & Interceptor)? { + if engine is Engine { + (readonly & Interceptor)[] interceptors = engine.getInterceptors(); + if interceptors.length() > self.getInterceptorCount() { + (readonly & Interceptor) next = interceptors[self.getInterceptorCount()]; + if !isGlobalInterceptor(next) && self.getPath().length() > 1 { + self.increaseInterceptorCount(); + return self.getNextInterceptor(engine); + } + self.increaseInterceptorCount(); + return next; + } + int nextFieldInterceptor = self.getInterceptorCount() - engine.getInterceptors().length(); + if self.getFieldInterceptors().length() > nextFieldInterceptor { + readonly & Interceptor next = self.getFieldInterceptors()[nextFieldInterceptor]; + self.increaseInterceptorCount(); + return next; + } + } + self.resetInterceptorCount(); + return; + } + + isolated function resetInterceptorCount() { + self.nextInterceptor = 0; + } + + isolated function getInterceptorCount() returns int { + return self.nextInterceptor; + } + + isolated function increaseInterceptorCount() { + self.nextInterceptor += 1; + } } diff --git a/ballerina/modules/parser/operation_node.bal b/ballerina/modules/parser/operation_node.bal index a4f687a40..ca9386517 100644 --- a/ballerina/modules/parser/operation_node.bal +++ b/ballerina/modules/parser/operation_node.bal @@ -26,8 +26,8 @@ public readonly class OperationNode { private boolean cofiguredInSchema; public isolated function init(string name, RootOperationType kind, Location location, - map variables = {}, SelectionNode[] selections = [], - DirectiveNode[] directives = []) { + map variables = {}, SelectionNode[] selections = [], + DirectiveNode[] directives = []) { self.name = name; self.kind = kind; self.location = location.cloneReadOnly(); @@ -70,7 +70,7 @@ public readonly class OperationNode { } public isolated function modifyWith(map variables, SelectionNode[] selections, - DirectiveNode[] directives) returns OperationNode { + DirectiveNode[] directives) returns OperationNode { return new (self.name, self.kind, self.location, variables, selections, directives); } diff --git a/ballerina/response_generator.bal b/ballerina/response_generator.bal index d3e7180bc..b7324f261 100644 --- a/ballerina/response_generator.bal +++ b/ballerina/response_generator.bal @@ -18,85 +18,84 @@ import graphql.parser; import ballerina/log; -class ResponseGenerator { +isolated class ResponseGenerator { private final Engine engine; private final Context context; - private (string|int)[] path; - private final __Type fieldType; + private final readonly & (string|int)[] path; + private final readonly & __Type fieldType; private final readonly & ServerCacheConfig? cacheConfig; private final readonly & string[] parentArgHashes; private final string functionNameGetFragmentFromService = ""; - isolated function init(Engine engine, Context context, __Type fieldType, (string|int)[] path = [], + isolated function init(Engine engine, Context context, __Type fieldType, readonly & (string|int)[] path = [], ServerCacheConfig? cacheConfig = (), readonly & string[] parentArgHashes = []) { self.engine = engine; self.context = context; self.path = path; - self.fieldType = fieldType; + self.fieldType = fieldType.cloneReadOnly(); self.cacheConfig = cacheConfig; self.parentArgHashes = parentArgHashes; } - isolated function getResult(any|error parentValue, parser:FieldNode parentNode) + isolated function getResult(any|error parentValue, parser:FieldNode parentNode, (string|int)[] path = []) returns anydata { if parentValue is ErrorDetail { return; } if parentValue is error { - return self.addError(parentValue, parentNode); + return self.addError(parentValue, parentNode, path); } if parentValue is Scalar || parentValue is Scalar[] { return parentValue; } if parentValue is map { if isMap(parentValue) { - return self.getResultFromMap(parentValue, parentNode); + return self.getResultFromMap(parentValue, parentNode, path); } - return self.getResultFromRecord(parentValue, parentNode); + return self.getResultFromRecord(parentValue, parentNode, path); } if parentValue is (any|error)[] { - return self.getResultFromArray(parentValue, parentNode); + return self.getResultFromArray(parentValue, parentNode, path); } if parentValue is table> { - return self.getResultFromTable(parentValue, parentNode); + return self.getResultFromTable(parentValue, parentNode, path); } if parentValue is service object {} { - return self.getResultFromService(parentValue, parentNode); + return self.getResultFromService(parentValue, parentNode, path); } } - isolated function getResultFromObject(any parentValue, parser:FieldNode fieldNode) returns anydata { + isolated function getResultFromObject(any parentValue, parser:FieldNode fieldNode, (string|int)[] path = []) + returns anydata { if fieldNode.getName() == TYPE_NAME_FIELD { return getTypeNameFromValue(parentValue); } else if parentValue is map { if parentValue.hasKey(fieldNode.getName()) { - return self.getResult(parentValue.get(fieldNode.getName()), fieldNode); + return self.getResult(parentValue.get(fieldNode.getName()), fieldNode, path); } else if parentValue.hasKey(fieldNode.getAlias()) { // TODO: This is to handle results from hierarchical paths. Should find a better way to handle this. - return self.getResult(parentValue.get(fieldNode.getAlias()), fieldNode); + return self.getResult(parentValue.get(fieldNode.getAlias()), fieldNode, path); } else { return; } } else if parentValue is service object {} { - (string|int)[] clonedPath = self.path.clone(); - clonedPath.push(fieldNode.getAlias()); + readonly & (string|int)[] clonedPath = [...self.path, ...path, fieldNode.getAlias()]; __Type parentType = self.fieldType; __Type fieldType = getFieldTypeFromParentType(parentType, self.engine.getSchema().types, fieldNode); Field 'field = new (fieldNode, fieldType, parentType, parentValue, clonedPath, cacheConfig = self.cacheConfig, parentArgHashes = self.parentArgHashes ); - self.context.resetInterceptorCount(); return self.engine.resolve(self.context, 'field); } } - isolated function addError(error err, parser:FieldNode fieldNode) returns ErrorDetail { + isolated function addError(error err, parser:FieldNode fieldNode, (string|int)[] path = []) returns ErrorDetail { log:printError(err.message(), stackTrace = err.stackTrace()); ErrorDetail errorDetail = { message: err.message(), locations: [fieldNode.getLocation()], - path: self.path.clone() + path: path.length() == 0 ? self.path.clone() : [...self.path, ...path] }; self.context.addError(errorDetail); return errorDetail; @@ -117,37 +116,37 @@ class ResponseGenerator { self.context.addErrors(errorDetails); } - isolated function getResultFromMap(map parentValue, parser:FieldNode parentNode) + isolated function getResultFromMap(map parentValue, parser:FieldNode parentNode, (string|int)[] path = []) returns anydata { string? mapKey = getKeyArgument(parentNode); if mapKey is string { if parentValue.hasKey(mapKey) { - return self.getResult(parentValue.get(mapKey), parentNode); + return self.getResult(parentValue.get(mapKey), parentNode, path); } else { string message = string `The field "${parentNode.getName()}" is a map, but it does not contain the key "${mapKey}"`; - var result = self.getResult(error(message), parentNode); - return result; + return self.getResult(error(message), parentNode, path); } } else if parentValue is map { return parentValue; } } - isolated function getResultFromRecord(map parentValue, parser:FieldNode parentNode) + isolated function getResultFromRecord(map parentValue, parser:FieldNode parentNode, (string|int)[] path = []) returns anydata { Data result = {}; foreach parser:SelectionNode selection in parentNode.getSelections() { if selection is parser:FieldNode { - anydata fieldValue = self.getRecordResult(parentValue, selection); + anydata fieldValue = self.getRecordResult(parentValue, selection, path); result[selection.getAlias()] = fieldValue is ErrorDetail ? () : fieldValue; } else if selection is parser:FragmentNode { - self.getResultForFragmentFromMap(parentValue, selection, result); + self.getResultForFragmentFromMap(parentValue, selection, result, path); } } return result; } - isolated function getRecordResult(map parentValue, parser:FieldNode fieldNode) returns anydata { + isolated function getRecordResult(map parentValue, parser:FieldNode fieldNode, (string|int)[] path = []) +returns anydata { if fieldNode.getName() == TYPE_NAME_FIELD { return getTypeNameFromValue(parentValue); } @@ -155,36 +154,37 @@ class ResponseGenerator { __Type parentType = self.fieldType; __Type fieldType = getFieldTypeFromParentType(parentType, self.engine.getSchema().types, fieldNode); boolean isAlreadyCached = isRecordWithNoOptionalFields(parentValue); - (string|int)[] clonedPath = self.path.clone(); - clonedPath.push(fieldNode.getAlias()); + readonly & (string|int)[] clonedPath = [...self.path, ...path, fieldNode.getAlias()]; Field 'field = new (fieldNode, fieldType, parentType, path = clonedPath, fieldValue = fieldValue, cacheConfig = self.cacheConfig, parentArgHashes = self.parentArgHashes, - isAlreadyCached = isAlreadyCached); - self.context.resetInterceptorCount(); + isAlreadyCached = isAlreadyCached + ); return self.engine.resolve(self.context, 'field); } - isolated function getResultFromArray((any|error)[] parentValue, parser:FieldNode parentNode) returns anydata { + isolated function getResultFromArray((any|error)[] parentValue, parser:FieldNode parentNode, + (string|int)[] path = []) returns anydata { int i = 0; anydata[] result = []; foreach any|error element in parentValue { - self.path.push(i); - anydata elementValue = self.getResult(element, parentNode); - i += 1; - _ = self.path.pop(); + path.push(i); + anydata elementValue = self.getResult(element, parentNode, path); if elementValue is ErrorDetail { result.push(()); } else { result.push(elementValue); } + _ = path.pop(); + i += 1; } return result; } - isolated function getResultFromTable(table> parentValue, parser:FieldNode parentNode) returns anydata { + isolated function getResultFromTable(table> parentValue, parser:FieldNode parentNode, + (string|int)[] path = []) returns anydata { anydata[] result = []; foreach map element in parentValue { - anydata elementValue = self.getResult(element, parentNode); + anydata elementValue = self.getResult(element, parentNode, path); if elementValue is ErrorDetail { result.push(()); } else { @@ -194,46 +194,117 @@ class ResponseGenerator { return result; } - isolated function getResultFromService(service object {} serviceObject, parser:FieldNode parentNode) returns anydata { + isolated function getResultFromService(service object {} serviceObject, parser:FieldNode parentNode, + (string|int)[] path = []) returns anydata { Data result = {}; + if serviceObject is isolated service object {} { + return self.executeResourcesParallely(serviceObject, parentNode, path); + } foreach parser:SelectionNode selection in parentNode.getSelections() { if selection is parser:FieldNode { - anydata selectionValue = self.getResultFromObject(serviceObject, selection); + anydata selectionValue = self.getResultFromObject(serviceObject, selection, path); result[selection.getAlias()] = selectionValue is ErrorDetail ? () : selectionValue; } else if selection is parser:FragmentNode { - self.getResultForFragmentFromService(serviceObject, selection, result); + self.getResultForFragmentFromService(serviceObject, selection, result, path); + } + } + return result; + } + + isolated function executeResourcesParallely(isolated service object {} serviceObject, + parser:SelectionNode parentNode, (string|int)[] path = []) returns Data { + Data result = {}; + [parser:FieldNode, future][] selectionFutures = []; + readonly & (string|int)[] clonedPath = [...path]; + foreach parser:SelectionNode selection in parentNode.getSelections() { + if selection is parser:FieldNode { + future 'future = start self.getResultFromObject(serviceObject, selection, clonedPath); + selectionFutures.push([selection, 'future]); + } else if selection is parser:FragmentNode { + self.getResultForFragmentFromServiceParallely(serviceObject, selection, result, clonedPath); + } + } + foreach [parser:FieldNode, future] [selection, 'future] in selectionFutures { + anydata|error fieldValue = wait 'future; + if fieldValue is error { + result[selection.getAlias()] = (); + ErrorDetail errorDetail = { + message: fieldValue.message(), + locations: [selection.getLocation()], + path: clonedPath + }; + lock { + self.context.addError(errorDetail); + } + continue; } + result[selection.getAlias()] = fieldValue is ErrorDetail ? () : fieldValue; } return result; } - isolated function getResultForFragmentFromMap(map parentValue, parser:FragmentNode parentNode, Data result) { + isolated function getResultForFragmentFromServiceParallely(isolated service object {} parentValue, + parser:FragmentNode parentNode, Data result, (string|int)[] path = []) { + string typeName = getTypeNameFromValue(parentValue); + if parentNode.getOnType() != typeName && !self.isPossibleTypeOfInterface(parentNode.getOnType(), typeName) { + return; + } + [parser:FieldNode, future][] selections = []; + readonly & (string|int)[] clonedPath = [...path]; + foreach parser:SelectionNode selection in parentNode.getSelections() { + if selection is parser:FieldNode { + future 'future = start self.getResultFromObject(parentValue, selection, clonedPath); + selections.push([selection, 'future]); + } else if selection is parser:FragmentNode { + self.getResultForFragmentFromServiceParallely(parentValue, selection, result, clonedPath); + } + } + foreach [parser:FieldNode, future] [selection, 'future] in selections { + anydata|error fieldValue = wait 'future; + if fieldValue is error { + result[selection.getAlias()] = (); + ErrorDetail errorDetail = { + message: fieldValue.message(), + locations: [selection.getLocation()], + path: clonedPath + }; + lock { + self.context.addError(errorDetail); + } + continue; + } + result[selection.getAlias()] = fieldValue is ErrorDetail ? () : fieldValue; + } + } + + isolated function getResultForFragmentFromMap(map parentValue, parser:FragmentNode parentNode, Data result, + (string|int)[] path = []) { string typeName = getTypeNameFromValue(parentValue); if parentNode.getOnType() != typeName { return; } foreach parser:SelectionNode selection in parentNode.getSelections() { if selection is parser:FieldNode { - anydata fieldValue = self.getRecordResult(parentValue, selection); + anydata fieldValue = self.getRecordResult(parentValue, selection, path); result[selection.getAlias()] = fieldValue is ErrorDetail ? () : fieldValue; } else if selection is parser:FragmentNode { - self.getResultForFragmentFromMap(parentValue, selection, result); + self.getResultForFragmentFromMap(parentValue, selection, result, path); } } } isolated function getResultForFragmentFromService(service object {} parentValue, parser:FragmentNode parentNode, - Data result) { + Data result, (string|int)[] path = []) { string typeName = getTypeNameFromValue(parentValue); if parentNode.getOnType() != typeName && !self.isPossibleTypeOfInterface(parentNode.getOnType(), typeName) { return; } foreach parser:SelectionNode selection in parentNode.getSelections() { if selection is parser:FieldNode { - anydata selectionValue = self.getResultFromObject(parentValue, selection); + anydata selectionValue = self.getResultFromObject(parentValue, selection, path); result[selection.getAlias()] = selectionValue is ErrorDetail ? () : selectionValue; } else if selection is parser:FragmentNode { - self.getResultForFragmentFromService(parentValue, selection, result); + self.getResultForFragmentFromService(parentValue, selection, result, path); } } } diff --git a/ballerina/tests/utils.bal b/ballerina/tests/utils.bal index f08ee611c..fb48e430a 100644 --- a/ballerina/tests/utils.bal +++ b/ballerina/tests/utils.bal @@ -57,7 +57,7 @@ isolated function getFieldNodesFromDocumentFile(string fileName) returns parser: return fieldNodes; } -isolated function getField(parser:FieldNode fieldNode, __Type fieldType, __Type parentType, string[] path, +isolated function getField(parser:FieldNode fieldNode, __Type fieldType, __Type parentType, readonly & string[] path, ServerCacheConfig? cacheConfig = ()) returns Field { return new (fieldNode, fieldType, parentType, path = path, cacheConfig = cacheConfig); } diff --git a/ballerina/value_tree_builder.bal b/ballerina/value_tree_builder.bal index 20520f9a3..b1a7f979a 100644 --- a/ballerina/value_tree_builder.bal +++ b/ballerina/value_tree_builder.bal @@ -15,18 +15,8 @@ // under the License. isolated class ValueTreeBuilder { - private final Context context; - private final Data placeholderTree; - - isolated function init(Context context, Data placeholderTree) { - self.context = context; - self.placeholderTree = placeholderTree.clone(); - } - - isolated function build() returns Data { - lock { - return self.buildValueTree(self.context, self.placeholderTree).clone(); - } + isolated function build(Context context, Data placeholderTree) returns Data { + return self.buildValueTree(context, placeholderTree); } isolated function buildValueTree(Context context, anydata partialValue) returns anydata { diff --git a/changelog.md b/changelog.md index 836dcad1b..d64719b48 100644 --- a/changelog.md +++ b/changelog.md @@ -6,6 +6,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## [Unreleased] +### Added +- [[#4122] Introduce Parallel Execution for GraphQL Resolvers](https://github.com/ballerina-platform/ballerina-library/issues/4122) + +## [1.13.1] - 2024-07-02 + ### Changed - [[#6652] Cache Entire Record Having All Non-Optional Fields Instead of Caching Each Field Separately](https://github.com/ballerina-platform/ballerina-library/issues/6652) diff --git a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/Context.java b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/Context.java new file mode 100644 index 000000000..3b8509873 --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/Context.java @@ -0,0 +1,209 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.graphql.runtime.engine; + +import io.ballerina.runtime.api.PredefinedTypes; +import io.ballerina.runtime.api.creators.TypeCreator; +import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.types.ArrayType; +import io.ballerina.runtime.api.values.BArray; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BString; +import io.ballerina.runtime.api.values.BValue; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This class provides native implementations of the Ballerina Context class. + */ +public class Context { + private final ConcurrentHashMap attributes = new ConcurrentHashMap<>(); + // Provides mapping between user defined id and DataLoader + private final ConcurrentHashMap idDataLoaderMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap uuidPlaceholderMap = new ConcurrentHashMap<>(); + private final ConcurrentHashMap unResolvedPlaceholders = new ConcurrentHashMap<>(); + private final AtomicBoolean containPlaceholders = new AtomicBoolean(false); + // Tracks the number of Placeholders needs to be resolved + private final AtomicInteger unResolvedPlaceholderCount = new AtomicInteger(0); + private final AtomicInteger unResolvedPlaceholderNodeCount = new AtomicInteger(0); + private static final String CONTEXT = "context"; + + private Context() { + } + + public static void initializeContext(BObject context) { + context.addNativeData(CONTEXT, new Context()); + } + + public static void registerDataLoader(BObject object, BString key, BObject dataLoader) { + Context context = (Context) object.getNativeData(CONTEXT); + context.registerDataLoader(key, dataLoader); + } + + public static void setAttribute(BObject object, BString key, Object value) { + Context context = (Context) object.getNativeData(CONTEXT); + context.setAttribute(key, value); + } + + public static Object getAttribute(BObject object, BString key) { + Context context = (Context) object.getNativeData(CONTEXT); + return context.getAttribute(key); + } + + public static Object removeAttribute(BObject object, BString key) { + Context context = (Context) object.getNativeData(CONTEXT); + return context.removeAttribute(key); + } + + public static BObject getDataLoader(BObject object, BString key) { + Context context = (Context) object.getNativeData(CONTEXT); + return context.getDataLoader(key); + } + + public static BArray getDataLoaderIds(BObject object) { + Context context = (Context) object.getNativeData(CONTEXT); + return context.getDataLoaderIds(); + } + + public static BArray getUnresolvedPlaceholders(BObject object) { + Context context = (Context) object.getNativeData(CONTEXT); + return context.getUnresolvedPlaceholders(); + } + + public static void removeAllUnresolvedPlaceholders(BObject object) { + Context context = (Context) object.getNativeData(CONTEXT); + context.removeAllUnresolvedPlaceholders(); + } + + public static BObject getPlaceholder(BObject object, BString uuid) { + Context context = (Context) object.getNativeData(CONTEXT); + return context.getPlaceholder(uuid); + } + + public static int getUnresolvedPlaceholderCount(BObject object) { + Context context = (Context) object.getNativeData(CONTEXT); + return context.getUnresolvedPlaceholderCount(); + } + + public static int getUnresolvedPlaceholderNodeCount(BObject object) { + Context context = (Context) object.getNativeData(CONTEXT); + return context.getUnresolvedPlaceholderNodeCount(); + } + + public static void decrementUnresolvedPlaceholderNodeCount(BObject object) { + Context context = (Context) object.getNativeData(CONTEXT); + context.decrementUnresolvedPlaceholderNodeCount(); + } + + public static void decrementUnresolvedPlaceholderCount(BObject object) { + Context context = (Context) object.getNativeData(CONTEXT); + context.decrementUnresolvedPlaceholderCount(); + } + + public static void addUnresolvedPlaceholder(BObject object, BString uuid, BObject placeholder) { + Context context = (Context) object.getNativeData(CONTEXT); + context.addUnresolvedPlaceholder(uuid, placeholder); + } + + public static boolean hasPlaceholders(BObject object) { + Context context = (Context) object.getNativeData(CONTEXT); + return context.hasPlaceholders(); + } + + public static void clearPlaceholders(BObject object) { + Context context = (Context) object.getNativeData(CONTEXT); + context.clearPlaceholders(); + } + + private void setAttribute(BString key, Object value) { + this.attributes.put(key, value); + } + + private Object getAttribute(BString key) { + return this.attributes.get(key); + } + + private Object removeAttribute(BString key) { + return this.attributes.remove(key); + } + + private void registerDataLoader(BString key, BObject dataLoader) { + this.idDataLoaderMap.put(key, dataLoader); + } + + private BObject getDataLoader(BString key) { + return this.idDataLoaderMap.get(key); + } + + private BArray getDataLoaderIds() { + BArray values = ValueCreator.createArrayValue(TypeCreator.createArrayType(PredefinedTypes.TYPE_STRING)); + this.idDataLoaderMap.forEach((key, value) -> values.append(key)); + return values; + } + + private BArray getUnresolvedPlaceholders() { + Object[] valueArray = this.unResolvedPlaceholders.values().toArray(); + ArrayType arrayType = TypeCreator.createArrayType(((BValue) valueArray[0]).getType()); + return ValueCreator.createArrayValue(valueArray, arrayType); + } + + private void removeAllUnresolvedPlaceholders() { + this.unResolvedPlaceholders.clear(); + } + + private BObject getPlaceholder(BString uuid) { + return this.uuidPlaceholderMap.remove(uuid); + } + + private int getUnresolvedPlaceholderCount() { + return this.unResolvedPlaceholderCount.get(); + } + + private int getUnresolvedPlaceholderNodeCount() { + return this.unResolvedPlaceholderNodeCount.get(); + } + + private void decrementUnresolvedPlaceholderNodeCount() { + this.unResolvedPlaceholderNodeCount.decrementAndGet(); + } + + private void decrementUnresolvedPlaceholderCount() { + this.unResolvedPlaceholderCount.decrementAndGet(); + } + + private void addUnresolvedPlaceholder(BString uuid, BObject placeholder) { + this.containPlaceholders.set(true); + this.uuidPlaceholderMap.put(uuid, placeholder); + this.unResolvedPlaceholders.put(uuid, placeholder); + this.unResolvedPlaceholderCount.incrementAndGet(); + this.unResolvedPlaceholderNodeCount.incrementAndGet(); + } + + private boolean hasPlaceholders() { + return this.containPlaceholders.get(); + } + + private void clearPlaceholders() { + this.unResolvedPlaceholders.clear(); + this.uuidPlaceholderMap.clear(); + this.containPlaceholders.set(false); + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ExecutorVisitor.java b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ExecutorVisitor.java new file mode 100644 index 000000000..0163369bc --- /dev/null +++ b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ExecutorVisitor.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://www.wso2.org). + * + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.ballerina.stdlib.graphql.runtime.engine; + +import io.ballerina.runtime.api.creators.ValueCreator; +import io.ballerina.runtime.api.values.BMap; +import io.ballerina.runtime.api.values.BObject; +import io.ballerina.runtime.api.values.BString; + +import java.util.concurrent.ConcurrentHashMap; + +import static io.ballerina.stdlib.graphql.runtime.utils.ModuleUtils.getModule; + +/** + * This class provides native implementations of the Ballerina ExecutorVisitor class. + */ +public class ExecutorVisitor { + private static final String DATA_MAP = "dataMap"; + private static final String DATA_RECORD_NAME = "Data"; + private static final Object NullObject = new Object(); + private final ConcurrentHashMap dataMap = new ConcurrentHashMap<>(); + + private ExecutorVisitor() { + } + + public static void initializeDataMap(BObject executorVisitor) { + executorVisitor.addNativeData(DATA_MAP, new ExecutorVisitor()); + } + + public static void addData(BObject executorVisitor, BString key, Object value) { + ExecutorVisitor visitor = (ExecutorVisitor) executorVisitor.getNativeData(DATA_MAP); + visitor.addData(key, value); + } + + public static BMap getDataMap(BObject executorVisitor) { + ExecutorVisitor visitor = (ExecutorVisitor) executorVisitor.getNativeData(DATA_MAP); + return visitor.getDataMap(); + } + + private void addData(BString key, Object value) { + dataMap.put(key, value == null ? NullObject : value); + } + + private BMap getDataMap() { + BMap data = ValueCreator.createRecordValue(getModule(), DATA_RECORD_NAME); + dataMap.forEach((key, value) -> data.put(key, value.equals(NullObject) ? null : value)); + return data; + } +} diff --git a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ListenerUtils.java b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ListenerUtils.java index 7ef7fa1a5..3681fff45 100644 --- a/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ListenerUtils.java +++ b/native/src/main/java/io/ballerina/stdlib/graphql/runtime/engine/ListenerUtils.java @@ -155,7 +155,7 @@ public static Object getHtmlContentFromResources(BString url, Object subscriptio } public static void createAndStartObserverContext(Environment environment, BObject context, BString serviceName, - BString operationType, BString operationName, BString moduleName, + BString operationType, BString operationName, BString moduleName, BString fileName, int startLine, int startColumn) { ObserverContext observerContext = new GraphqlObserverContext(operationName.getValue(), serviceName.getValue()); observerContext.setManuallyClosed(true); @@ -196,7 +196,7 @@ public static void stopObserverContext(Environment environment, BObject context) environment.setStrandLocal(KEY_OBSERVER_CONTEXT, parentContext); } } - + public static void stopObserverContextWithError(Environment environment, BObject context, BError error) { ObserverContext observerContext = (ObserverContext) context.getNativeData(KEY_OBSERVER_CONTEXT); if (observerContext != null && observerContext.isManuallyClosed()) {