diff --git a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/instrumentation/TrackTimesInvokedInstrumentation.kt b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/instrumentation/TrackTimesInvokedInstrumentation.kt index 53a1bba7e1..a7cb8c2d6c 100644 --- a/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/instrumentation/TrackTimesInvokedInstrumentation.kt +++ b/examples/server/spring-server/src/main/kotlin/com/expediagroup/graphql/examples/server/spring/instrumentation/TrackTimesInvokedInstrumentation.kt @@ -1,5 +1,5 @@ /* - * Copyright 2021 Expedia, Inc + * Copyright 2024 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import graphql.execution.instrumentation.InstrumentationContext import graphql.execution.instrumentation.InstrumentationState import graphql.execution.instrumentation.SimpleInstrumentationContext import graphql.execution.instrumentation.SimplePerformantInstrumentation +import graphql.execution.instrumentation.parameters.InstrumentationCreateStateParameters import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters import org.slf4j.LoggerFactory @@ -37,7 +38,7 @@ class TrackTimesInvokedInstrumentation : SimplePerformantInstrumentation() { private val logger = LoggerFactory.getLogger(TrackTimesInvokedInstrumentation::class.java) - override fun createState(): InstrumentationState = TrackTimesInvokedInstrumenationState() + override fun createState(parameters: InstrumentationCreateStateParameters): InstrumentationState = TrackTimesInvokedInstrumenationState() override fun beginFieldFetch(parameters: InstrumentationFieldFetchParameters, state: InstrumentationState?): InstrumentationContext { if (parameters.field.getDirective(TRACK_TIMES_INVOKED_DIRECTIVE_NAME) != null) { diff --git a/executions/graphql-kotlin-automatic-persisted-queries/src/main/kotlin/com/expediagroup/graphql/apq/cache/AutomaticPersistedQueriesCache.kt b/executions/graphql-kotlin-automatic-persisted-queries/src/main/kotlin/com/expediagroup/graphql/apq/cache/AutomaticPersistedQueriesCache.kt index 0ba92960dd..ca8aa1fb84 100644 --- a/executions/graphql-kotlin-automatic-persisted-queries/src/main/kotlin/com/expediagroup/graphql/apq/cache/AutomaticPersistedQueriesCache.kt +++ b/executions/graphql-kotlin-automatic-persisted-queries/src/main/kotlin/com/expediagroup/graphql/apq/cache/AutomaticPersistedQueriesCache.kt @@ -23,18 +23,6 @@ import graphql.execution.preparsed.persisted.PersistedQueryCacheMiss import java.util.concurrent.CompletableFuture interface AutomaticPersistedQueriesCache : PersistedQueryCache { - - @Deprecated( - message = "deprecated in favor of async retrieval of PreparsedDocumentEntry", - replaceWith = ReplaceWith("getPersistedQueryDocumentAsync(persistedQueryId, executionInput, onCacheMiss)") - ) - override fun getPersistedQueryDocument( - persistedQueryId: Any, - executionInput: ExecutionInput, - onCacheMiss: PersistedQueryCacheMiss - ): PreparsedDocumentEntry = - getPersistedQueryDocumentAsync(persistedQueryId, executionInput, onCacheMiss).get() - override fun getPersistedQueryDocumentAsync( persistedQueryId: Any, executionInput: ExecutionInput, diff --git a/executions/graphql-kotlin-automatic-persisted-queries/src/main/kotlin/com/expediagroup/graphql/apq/provider/AutomaticPersistedQueriesProvider.kt b/executions/graphql-kotlin-automatic-persisted-queries/src/main/kotlin/com/expediagroup/graphql/apq/provider/AutomaticPersistedQueriesProvider.kt index ca67806bfa..0e6bb3662e 100644 --- a/executions/graphql-kotlin-automatic-persisted-queries/src/main/kotlin/com/expediagroup/graphql/apq/provider/AutomaticPersistedQueriesProvider.kt +++ b/executions/graphql-kotlin-automatic-persisted-queries/src/main/kotlin/com/expediagroup/graphql/apq/provider/AutomaticPersistedQueriesProvider.kt @@ -33,20 +33,6 @@ import java.util.function.Function class AutomaticPersistedQueriesProvider( private val cache: AutomaticPersistedQueriesCache ) : PreparsedDocumentProvider { - - @Deprecated( - "deprecated in favor of async retrieval of Document", - ReplaceWith("this.getDocumentAsync(executionInput, parseAndValidateFunction).get()") - ) - override fun getDocument( - executionInput: ExecutionInput, - parseAndValidateFunction: Function - ): PreparsedDocumentEntry = - this.getDocumentAsync( - executionInput, - parseAndValidateFunction - ).get() - override fun getDocumentAsync( executionInput: ExecutionInput, parseAndValidateFunction: Function diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/extensions/CompletableFutureExtensions.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/extensions/CompletableFutureExtensions.kt index 98df382ed5..02d2faab25 100644 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/extensions/CompletableFutureExtensions.kt +++ b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/extensions/CompletableFutureExtensions.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Expedia, Inc + * Copyright 2024 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,8 +19,6 @@ package com.expediagroup.graphql.dataloader.instrumentation.extensions import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistry import com.expediagroup.graphql.dataloader.instrumentation.exceptions.MissingInstrumentationStateException import com.expediagroup.graphql.dataloader.instrumentation.exceptions.MissingKotlinDataLoaderRegistryException -import com.expediagroup.graphql.dataloader.instrumentation.level.state.ExecutionLevelDispatchedState -import com.expediagroup.graphql.dataloader.instrumentation.level.state.Level import com.expediagroup.graphql.dataloader.instrumentation.syncexhaustion.state.SyncExecutionExhaustedState import graphql.schema.DataFetchingEnvironment import org.dataloader.DataLoader @@ -38,11 +36,6 @@ fun CompletableFuture.dispatchIfNeeded( if (dataLoaderRegistry.dataLoadersInvokedOnDispatch()) { val cantContinueExecution = when { - environment.graphQlContext.hasKey(ExecutionLevelDispatchedState::class) -> { - environment - .graphQlContext.get(ExecutionLevelDispatchedState::class) - .allExecutionsDispatched(Level(environment.executionStepInfo.path.level)) - } environment.graphQlContext.hasKey(SyncExecutionExhaustedState::class) -> { environment .graphQlContext.get(SyncExecutionExhaustedState::class) diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/DataLoaderLevelDispatchedInstrumentation.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/DataLoaderLevelDispatchedInstrumentation.kt deleted file mode 100644 index 00b88c36a1..0000000000 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/DataLoaderLevelDispatchedInstrumentation.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2024 Expedia, Inc - * - * Licensed 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 - * - * https://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 com.expediagroup.graphql.dataloader.instrumentation.level - -import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistry -import com.expediagroup.graphql.dataloader.instrumentation.level.execution.AbstractExecutionLevelDispatchedInstrumentation -import com.expediagroup.graphql.dataloader.instrumentation.level.execution.ExecutionLevelDispatchedInstrumentationParameters -import com.expediagroup.graphql.dataloader.instrumentation.level.execution.OnLevelDispatchedCallback -import com.expediagroup.graphql.dataloader.instrumentation.level.state.Level -import graphql.ExecutionInput -import graphql.GraphQLContext -import graphql.execution.instrumentation.Instrumentation -import graphql.schema.DataFetcher -import org.dataloader.DataLoader - -/** - * Custom GraphQL [Instrumentation] that will dispatch all [DataLoader]s inside a [KotlinDataLoaderRegistry] - * when certain [Level] is dispatched for all [ExecutionInput] sharing a [GraphQLContext] - * - * A level is considered Dispatched when all [DataFetcher]s of a particular level of all [ExecutionInput]s - * were dispatched - */ -class DataLoaderLevelDispatchedInstrumentation : AbstractExecutionLevelDispatchedInstrumentation() { - override fun getOnLevelDispatchedCallback( - parameters: ExecutionLevelDispatchedInstrumentationParameters - ): OnLevelDispatchedCallback = { _, _ -> - parameters.executionContext.executionInput - ?.dataLoaderRegistry - ?.dispatchAll() - } -} diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/execution/AbstractExecutionLevelDispatchedInstrumentation.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/execution/AbstractExecutionLevelDispatchedInstrumentation.kt deleted file mode 100644 index dfef0e2287..0000000000 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/execution/AbstractExecutionLevelDispatchedInstrumentation.kt +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Copyright 2024 Expedia, Inc - * - * Licensed 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 - * - * https://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 com.expediagroup.graphql.dataloader.instrumentation.level.execution - -import com.expediagroup.graphql.dataloader.instrumentation.extensions.isMutation -import com.expediagroup.graphql.dataloader.instrumentation.level.state.ExecutionLevelDispatchedState -import com.expediagroup.graphql.dataloader.instrumentation.level.state.Level -import graphql.ExecutionInput -import graphql.ExecutionResult -import graphql.execution.ExecutionContext -import graphql.execution.ExecutionId -import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext -import graphql.execution.instrumentation.InstrumentationContext -import graphql.execution.instrumentation.InstrumentationState -import graphql.execution.instrumentation.SimplePerformantInstrumentation -import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters -import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters -import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters -import graphql.schema.DataFetcher - -/** - * Represents the signature of a callback that will be executed when a [Level] is dispatched - */ -internal typealias OnLevelDispatchedCallback = (Level, List) -> Unit -/** - * Custom GraphQL [graphql.execution.instrumentation.Instrumentation] that calculate the state of executions - * of all queries sharing the same GraphQLContext map - */ -abstract class AbstractExecutionLevelDispatchedInstrumentation : SimplePerformantInstrumentation() { - /** - * This is invoked each time instrumentation attempts to calculate a level dispatched state, this can be called from either - * `beginFieldField` or `beginExecutionStrategy`. - * - * @param parameters contains information of which [ExecutionInput] caused the calculation and from which hook - * @return [OnLevelDispatchedCallback] to invoke a method when a certain level of all operations dispatched - * like `onDispatched` - */ - abstract fun getOnLevelDispatchedCallback( - parameters: ExecutionLevelDispatchedInstrumentationParameters - ): OnLevelDispatchedCallback - - override fun beginExecution( - parameters: InstrumentationExecutionParameters, - state: InstrumentationState? - ): InstrumentationContext? = - parameters.executionInput - ?.graphQLContext?.get(ExecutionLevelDispatchedState::class) - ?.beginExecution(parameters) - - override fun beginExecutionStrategy( - parameters: InstrumentationExecutionStrategyParameters, - state: InstrumentationState? - ): ExecutionStrategyInstrumentationContext? = - parameters.executionContext.takeUnless(ExecutionContext::isMutation) - ?.graphQLContext?.get(ExecutionLevelDispatchedState::class) - ?.beginExecutionStrategy( - parameters, - this.getOnLevelDispatchedCallback( - ExecutionLevelDispatchedInstrumentationParameters( - parameters.executionContext, - ExecutionLevelCalculationSource.EXECUTION_STRATEGY - ) - ) - ) - - override fun beginFieldFetch( - parameters: InstrumentationFieldFetchParameters, - state: InstrumentationState? - ): InstrumentationContext? = - parameters.executionContext.takeUnless(ExecutionContext::isMutation) - ?.graphQLContext?.get(ExecutionLevelDispatchedState::class) - ?.beginFieldFetch( - parameters, - this.getOnLevelDispatchedCallback( - ExecutionLevelDispatchedInstrumentationParameters( - parameters.executionContext, - ExecutionLevelCalculationSource.FIELD_FETCH - ) - ) - ) - - override fun instrumentDataFetcher( - dataFetcher: DataFetcher<*>, - parameters: InstrumentationFieldFetchParameters, - state: InstrumentationState? - ): DataFetcher<*> = - parameters.executionContext.takeUnless(ExecutionContext::isMutation) - ?.graphQLContext?.get(ExecutionLevelDispatchedState::class) - ?.instrumentDataFetcher(dataFetcher, parameters) - ?: dataFetcher -} diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/execution/ExecutionLevelDispatchedInstrumentationParameters.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/execution/ExecutionLevelDispatchedInstrumentationParameters.kt deleted file mode 100644 index a33e278b80..0000000000 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/execution/ExecutionLevelDispatchedInstrumentationParameters.kt +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2022 Expedia, Inc - * - * Licensed 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 - * - * https://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 com.expediagroup.graphql.dataloader.instrumentation.level.execution - -import graphql.execution.ExecutionContext - -/** - * Source of level dispatched state calculation - */ -enum class ExecutionLevelCalculationSource { EXECUTION_STRATEGY, FIELD_FETCH } - -/** - * Hold information related to from where the level dispatched state is being calculated - */ -data class ExecutionLevelDispatchedInstrumentationParameters( - val executionContext: ExecutionContext, - val calculationSource: ExecutionLevelCalculationSource -) diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ExecutionBatchState.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ExecutionBatchState.kt deleted file mode 100644 index f96e7745d5..0000000000 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ExecutionBatchState.kt +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2023 Expedia, Inc - * - * Licensed 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 - * - * https://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 com.expediagroup.graphql.dataloader.instrumentation.level.state - -import graphql.schema.DataFetcher -import graphql.schema.LightDataFetcher - -enum class LevelState { NOT_DISPATCHED, DISPATCHED } - -/** - * Handle the state of an [graphql.ExecutionInput] - */ -class ExecutionBatchState { - private val levelsState: MutableMap = mutableMapOf() - - private val expectedFetches: MutableMap = mutableMapOf() - private val dispatchedFetches: MutableMap = mutableMapOf() - - private val expectedExecutionStrategies: MutableMap = mutableMapOf() - private val dispatchedExecutionStrategies: MutableMap = mutableMapOf() - - private val onFieldValueInfos: MutableMap = mutableMapOf() - - private val manuallyCompletableDataFetchers: MutableMap> = mutableMapOf() - - /** - * Initializes a level state in the [ExecutionBatchState] - * @param level to be initialized - */ - fun initializeLevelStateIfNeeded(level: Level) { - if (!this.contains(level)) { - levelsState[level] = LevelState.NOT_DISPATCHED - expectedFetches[level] = 0 - dispatchedFetches[level] = 0 - expectedExecutionStrategies[level] = if (level.isFirst()) 1 else 0 - dispatchedExecutionStrategies[level] = 0 - onFieldValueInfos[level] = 0 - manuallyCompletableDataFetchers[level] = mutableListOf() - } - } - - /** - * Check if the [ExecutionBatchState] contains a level - * - * @param level to check if his state is being calculated - * @return whether state contains the level - */ - fun contains(level: Level): Boolean = levelsState.containsKey(level) - - /** - * Increase fetches that this [ExecutionBatchState] is expecting - * - * @param level which level expects [count] of fetches - * @param count how many more fetches the [level] will expect - * @return total expected fetches - */ - fun increaseExpectedFetches(level: Level, count: Int): Int? = - expectedFetches.computeIfPresent(level) { _, currentCount -> currentCount + count } - - /** - * Increase dispatched fetches of this [ExecutionBatchState] - * - * @param level which level should increase dispatched fetches - * @return total dispatched fetches - */ - fun increaseDispatchedFetches(level: Level): Int? = - dispatchedFetches.computeIfPresent(level) { _, currentCount -> currentCount + 1 } - - /** - * Increase executionStrategies that this [ExecutionBatchState] is expecting - * - * @param level which level expects [count] of fetches - * @param count how many more executionStrategies the [level] will expect - * @return total expected executionStrategies - */ - fun increaseExpectedExecutionStrategies(level: Level, count: Int): Int? = - expectedExecutionStrategies.computeIfPresent(level) { _, currentCount -> currentCount + count } - - /** - * Increase dispatched executionStrategies of this [ExecutionBatchState] - * - * @param level which level should increase dispatched fetches - * @return total dispatched executionStrategies - */ - fun increaseDispatchedExecutionStrategies(level: Level): Int? = - dispatchedExecutionStrategies.computeIfPresent(level) { _, currentCount -> currentCount + 1 } - - /** - * Increase OnFieldValueInfos invocations of this [ExecutionBatchState] - * - * @param level which level should increase OnFieldValueInfos invocations - * @return total onFieldValueInfos invocations - */ - fun increaseOnFieldValueInfos(level: Level): Int? = - onFieldValueInfos.computeIfPresent(level) { _, currentCount -> currentCount + 1 } - - /** - * Instrument a dataFetcher to modify his runtime behavior to manually complete the returned CompletableFuture - * - * @param level which level the [dataFetcher] belongs - * @param dataFetcher to be instrumented - * @return instrumented dataFetcher - */ - fun toManuallyCompletableDataFetcher(level: Level, dataFetcher: DataFetcher<*>): ManualDataFetcher { - val manualDataFetcher = when (dataFetcher) { - is LightDataFetcher<*> -> ManuallyCompletableLightDataFetcher(dataFetcher) - else -> ManuallyCompletableDataFetcher(dataFetcher) - } - manuallyCompletableDataFetchers[level]?.add(manualDataFetcher) - return manualDataFetcher - } - - /** - * Complete all the [manuallyCompletableDataFetchers] - * - * @param level which level should complete dataFetchers - */ - fun completeDataFetchers(level: Level) { - manuallyCompletableDataFetchers[level]?.forEach(ManualDataFetcher::complete) - } - - /** - * Check if a given level is dispatched - * - * @param level which level check if its dispatched - */ - fun isLevelDispatched(level: Level): Boolean = when { - levelsState[level] == LevelState.DISPATCHED -> true - level.isFirst() -> dispatchedFetches[level] == expectedFetches[level] - else -> { - val previousLevel = level.previous() - isLevelDispatched(previousLevel) && - onFieldValueInfos[previousLevel] == expectedExecutionStrategies[previousLevel] && - dispatchedExecutionStrategies[level] == expectedExecutionStrategies[level] && - dispatchedFetches[level] == expectedFetches[level] - } - }.also { isLevelDispatched -> - if (isLevelDispatched) { - levelsState[level] = LevelState.DISPATCHED - } - } -} diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ExecutionLevelDispatchedState.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ExecutionLevelDispatchedState.kt deleted file mode 100644 index db93a0b514..0000000000 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ExecutionLevelDispatchedState.kt +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright 2024 Expedia, Inc - * - * Licensed 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 - * - * https://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 com.expediagroup.graphql.dataloader.instrumentation.level.state - -import com.expediagroup.graphql.dataloader.instrumentation.extensions.getExpectedStrategyCalls -import com.expediagroup.graphql.dataloader.instrumentation.level.execution.OnLevelDispatchedCallback -import graphql.ExecutionInput -import graphql.ExecutionResult -import graphql.execution.ExecutionId -import graphql.execution.FieldValueInfo -import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext -import graphql.execution.instrumentation.InstrumentationContext -import graphql.execution.instrumentation.SimpleInstrumentationContext -import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters -import graphql.execution.instrumentation.parameters.InstrumentationExecutionStrategyParameters -import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters -import graphql.schema.DataFetcher -import java.util.concurrent.CompletableFuture -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.atomic.AtomicReference - -/** - * Orchestrate the [ExecutionBatchState] of all [ExecutionInput] sharing the same graphQLContext map, - * when a certain state is reached will invoke [OnLevelDispatchedCallback] - */ -class ExecutionLevelDispatchedState( - totalOperations: Int -) { - private val totalExecutions: AtomicReference = AtomicReference(totalOperations) - val executions = ConcurrentHashMap() - - /** - * Remove an [ExecutionBatchState] from the state in case operation does not qualify for execution, - * for example: - * parsing, validation, execution errors - * persisted query errors - */ - private fun removeExecution(executionId: ExecutionId) { - if (executions.containsKey(executionId)) { - executions.remove(executionId) - totalExecutions.set(totalExecutions.get() - 1) - } - } - - /** - * Initialize the [ExecutionBatchState] of this [ExecutionInput] - * - * @param parameters contains information of which [ExecutionInput] will start his execution - * @return a nullable [InstrumentationContext] - */ - fun beginExecution( - parameters: InstrumentationExecutionParameters - ): InstrumentationContext { - executions.computeIfAbsent(parameters.executionInput.executionId) { - ExecutionBatchState() - } - return object : SimpleInstrumentationContext() { - override fun onCompleted(result: ExecutionResult?, t: Throwable?) { - result?.let { - if (result.errors.size > 0) { - removeExecution(parameters.executionInput.executionId) - } - } - } - } - } - - /** - * When a specific [ExecutionInput] begins an executionStrategy, modify the state of his [ExecutionBatchState] - * - * @param parameters contains information of which [ExecutionInput] will start an ExecutionStrategy - * @param onLevelDispatched callback invoke when certain level of all operations is dispatched - */ - fun beginExecutionStrategy( - parameters: InstrumentationExecutionStrategyParameters, - onLevelDispatched: OnLevelDispatchedCallback - ): ExecutionStrategyInstrumentationContext { - val executionId = parameters.executionContext.executionInput.executionId - val level = Level(parameters.executionStrategyParameters.path.level + 1) - val fieldCount = parameters.executionStrategyParameters.fields.size() - - executions.computeIfPresent(executionId) { _, executionState -> - executionState.also { - it.initializeLevelStateIfNeeded(level) - it.increaseExpectedFetches(level, fieldCount) - it.increaseDispatchedExecutionStrategies(level) - } - } - - return object : ExecutionStrategyInstrumentationContext { - override fun onDispatched(result: CompletableFuture) { - } - - override fun onCompleted(result: ExecutionResult?, t: Throwable?) { - } - - override fun onFieldValuesInfo(fieldValueInfoList: List) { - val nextLevel = level.next() - - executions.computeIfPresent(executionId) { _, executionState -> - executionState.also { - it.increaseOnFieldValueInfos(level) - it.increaseExpectedExecutionStrategies( - nextLevel, - fieldValueInfoList.getExpectedStrategyCalls() - ) - } - } - - val allExecutionsDispatched = synchronized(executions) { allExecutionsDispatched(nextLevel) } - if (allExecutionsDispatched) { - onLevelDispatched(nextLevel, executions.keys().toList()) - executions.forEach { (_, executionState) -> executionState.completeDataFetchers(nextLevel) } - } - } - - override fun onFieldValuesException() { - executions.computeIfPresent(executionId) { _, executionState -> - executionState.also { - it.increaseOnFieldValueInfos(level) - } - } - } - } - } - - /** - * When a specific [ExecutionInput] begins an fieldFetch, modify the state of his [ExecutionBatchState] - * - * @param parameters contains information of which [ExecutionInput] will start an ExecutionStrategy - * @param onLevelDispatched invoke when certain level of all operations is dispatched - */ - fun beginFieldFetch( - parameters: InstrumentationFieldFetchParameters, - onLevelDispatched: OnLevelDispatchedCallback - ): InstrumentationContext { - val executionId = parameters.executionContext.executionInput.executionId - val path = parameters.executionStepInfo.path - val level = Level(path.level) - - return object : SimpleInstrumentationContext() { - override fun onDispatched(result: CompletableFuture) { - executions.computeIfPresent(executionId) { _, executionState -> - executionState.also { it.increaseDispatchedFetches(level) } - } - - val allExecutionsDispatched = synchronized(executions) { allExecutionsDispatched(level) } - if (allExecutionsDispatched) { - onLevelDispatched.invoke(level, executions.keys().toList()) - executions.forEach { (_, executionState) -> executionState.completeDataFetchers(level) } - } - } - } - } - - /** - * Modify runtime behaviour of ExecutionStrategy by instrumenting a data fetcher that can be - * manually completed, by default ExecutionStrategy will do a Depth First execution, by instrumenting - * the [dataFetcher] it will switch to Breath First execution to complete when a level of all operations sharing - * a graphQLContext was dispatched - * - * @param dataFetcher the original dataFetcher that will be instrumented - * @param parameters contains information of which [ExecutionInput] will use the [dataFetcher] - * @return [ManuallyCompletableDataFetcher] - */ - fun instrumentDataFetcher( - dataFetcher: DataFetcher<*>, - parameters: InstrumentationFieldFetchParameters - ): DataFetcher<*> { - var manuallyCompletableDataFetcher: DataFetcher<*> = dataFetcher - executions.computeIfPresent(parameters.executionContext.executionInput.executionId) { _, executionState -> - executionState.also { - manuallyCompletableDataFetcher = it.toManuallyCompletableDataFetcher( - Level(parameters.executionStepInfo.path.level), - dataFetcher - ) - } - } - return manuallyCompletableDataFetcher - } - - /** - * calculate if all executions sharing a graphQLContext was dispatched, by - * 1. Checking if all executions started. - * 3. check if all executions dispatched the provided [level]. - * - * @param level that execution state will be calculated - * @return Boolean for allExecutionsDispatched statement - */ - fun allExecutionsDispatched(level: Level): Boolean = synchronized(executions) { - val operationsToExecute = totalExecutions.get() - when { - executions.size < operationsToExecute -> false - else -> executions.all { (_, executionState) -> executionState.isLevelDispatched(level) } - } - } -} diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/Level.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/Level.kt deleted file mode 100644 index 5426703142..0000000000 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/Level.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2023 Expedia, Inc - * - * Licensed 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 - * - * https://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 com.expediagroup.graphql.dataloader.instrumentation.level.state - -/** - * Represent a Level of a Document - */ -data class Level(private val number: Int) { - /** - * calculate [Level] that is after this [Level] - * @return next [Level] - */ - fun next(): Level = Level(number + 1) - - /** - * calculate [Level] that is before this [Level] - * @return previous [Level] - */ - fun previous(): Level = Level(number - 1) - - /** - * calculate if this [Level] is the first - * @return whether this is the first [Level] - */ - fun isFirst(): Boolean = number == 1 -} diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ManualDataFetcher.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ManualDataFetcher.kt deleted file mode 100644 index 070d8b5956..0000000000 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ManualDataFetcher.kt +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Copyright 2023 Expedia, Inc - * - * Licensed 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 - * - * https://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 com.expediagroup.graphql.dataloader.instrumentation.level.state - -import graphql.schema.DataFetcher -import java.util.concurrent.CompletableFuture - -/** - * DataFetcher Decorator that allows manual completion of dataFetchers - */ -abstract class ManualDataFetcher : DataFetcher> { - val manualFuture: CompletableFuture = CompletableFuture() - var originalFuture: CompletableFuture? = null - var originalExpressionException: Exception? = null - - /** - * Manually complete the [manualFuture] by handling the [originalFuture] - */ - fun complete() { - when { - originalExpressionException != null -> manualFuture.completeExceptionally(originalExpressionException) - else -> originalFuture?.handle { result, exception -> - when { - exception != null -> manualFuture.completeExceptionally(exception) - else -> manualFuture.complete(result) - } - } - } - } -} diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ManuallyCompletableDataFetcher.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ManuallyCompletableDataFetcher.kt deleted file mode 100644 index d1db8b729b..0000000000 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ManuallyCompletableDataFetcher.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2023 Expedia, Inc - * - * Licensed 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 - * - * https://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 com.expediagroup.graphql.dataloader.instrumentation.level.state - -import graphql.execution.Async -import graphql.schema.DataFetcher -import graphql.schema.DataFetchingEnvironment -import java.util.concurrent.CompletableFuture - -/** - * DataFetcher Decorator that stores the original dataFetcher result (it's always a completable future) - * it stores the [originalFuture] as property and returns an uncompleted [manualFuture] - * then at later point manually call [complete] to complete the [manualFuture] with the [originalFuture] result - * to let ExecutionStrategy handle all futures - * - * @param originalDataFetcher original dataFetcher to be decorated - */ -class ManuallyCompletableDataFetcher( - private val originalDataFetcher: DataFetcher<*> -) : ManualDataFetcher() { - /** - * when attempting to get the value from dataFetcher, execute the [originalDataFetcher] - * and store the resulting future [originalFuture] and a possible [originalExpressionException] if - * a synchronous exception was thrown during the execution - * - * @param environment dataFetchingEnvironment with information about the field - * @return an uncompleted manualFuture that can be completed at later time - */ - override fun get(environment: DataFetchingEnvironment): CompletableFuture { - try { - val fetchedValueRaw = originalDataFetcher.get(environment) - originalFuture = Async.toCompletableFuture(fetchedValueRaw) - } catch (e: Exception) { - originalExpressionException = e - } - return manualFuture - } -} diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ManuallyCompletableLightDataFetcher.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ManuallyCompletableLightDataFetcher.kt deleted file mode 100644 index 397b979377..0000000000 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/state/ManuallyCompletableLightDataFetcher.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2023 Expedia, Inc - * - * Licensed 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 - * - * https://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 com.expediagroup.graphql.dataloader.instrumentation.level.state - -import graphql.execution.Async -import graphql.schema.DataFetchingEnvironment -import graphql.schema.GraphQLFieldDefinition -import graphql.schema.LightDataFetcher -import java.util.concurrent.CompletableFuture -import java.util.function.Supplier - -/** - * LightDataFetcher Decorator that stores the original dataFetcher result (it's always a completable future) - * it stores the [originalFuture] as property and returns an uncompleted [manualFuture] - * then at later point manually call [complete] to complete the [manualFuture] with the [originalFuture] result - * to let ExecutionStrategy handle all futures - * - * @param originalDataFetcher original dataFetcher to be decorated - */ -class ManuallyCompletableLightDataFetcher( - private val originalDataFetcher: LightDataFetcher<*> -) : ManualDataFetcher(), LightDataFetcher> { - - override fun get(environment: DataFetchingEnvironment): CompletableFuture = - get(environment.fieldDefinition, environment.getSource()) { environment } - - /** - * when attempting to get the value from LightDataFetcher, execute the [originalDataFetcher] - * and store the resulting future [originalFuture] and a possible [originalExpressionException] if - * a synchronous exception was thrown during the execution - * - * @param fieldDefinition the graphql field definition - * @param sourceObject the source object to get a value from - * @param environmentSupplier a supplier of the [DataFetchingEnvironment] that creates it lazily - * @return an uncompleted manualFuture that can be completed at later time - */ - override fun get( - fieldDefinition: GraphQLFieldDefinition, - sourceObject: Any?, - environmentSupplier: Supplier - ): CompletableFuture { - try { - val fetchedValueRaw = originalDataFetcher.get( - fieldDefinition, - sourceObject, - environmentSupplier - ) - originalFuture = Async.toCompletableFuture(fetchedValueRaw) - } catch (e: Exception) { - originalExpressionException = e - } - return manualFuture - } -} diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/execution/AbstractSyncExecutionExhaustedInstrumentation.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/execution/AbstractSyncExecutionExhaustedInstrumentation.kt index 8d06df8f85..acb291e404 100644 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/execution/AbstractSyncExecutionExhaustedInstrumentation.kt +++ b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/execution/AbstractSyncExecutionExhaustedInstrumentation.kt @@ -23,7 +23,9 @@ import graphql.ExecutionResult import graphql.GraphQLContext import graphql.execution.ExecutionContext import graphql.execution.ExecutionId +import graphql.execution.instrumentation.ExecuteObjectInstrumentationContext import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext +import graphql.execution.instrumentation.FieldFetchingInstrumentationContext import graphql.execution.instrumentation.Instrumentation import graphql.execution.instrumentation.InstrumentationContext import graphql.execution.instrumentation.InstrumentationState @@ -64,18 +66,30 @@ abstract class AbstractSyncExecutionExhaustedInstrumentation : SimplePerformantI override fun beginExecutionStrategy( parameters: InstrumentationExecutionStrategyParameters, state: InstrumentationState? - ): ExecutionStrategyInstrumentationContext? = + ): ExecutionStrategyInstrumentationContext? { parameters.executionContext.takeUnless(ExecutionContext::isMutation) ?.graphQLContext?.get(SyncExecutionExhaustedState::class) - ?.beginExecutionStrategy(parameters) + ?.beginRecursiveExecution(parameters) + return null + } - override fun beginFieldFetch( + override fun beginExecuteObject( + parameters: InstrumentationExecutionStrategyParameters, + state: InstrumentationState? + ): ExecuteObjectInstrumentationContext? { + parameters.executionContext.takeUnless(ExecutionContext::isMutation) + ?.graphQLContext?.get(SyncExecutionExhaustedState::class) + ?.beginRecursiveExecution(parameters) + return null + } + + override fun beginFieldFetching( parameters: InstrumentationFieldFetchParameters, state: InstrumentationState? - ): InstrumentationContext? = + ): FieldFetchingInstrumentationContext? = parameters.executionContext.takeUnless(ExecutionContext::isMutation) ?.graphQLContext?.get(SyncExecutionExhaustedState::class) - ?.beginFieldFetch( + ?.beginFieldFetching( parameters, this.getOnSyncExecutionExhaustedCallback( SyncExecutionExhaustedInstrumentationParameters(parameters.executionContext) diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/state/ExecutionBatchState.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/state/ExecutionBatchState.kt index 7d5cae1986..677fff041d 100644 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/state/ExecutionBatchState.kt +++ b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/state/ExecutionBatchState.kt @@ -22,7 +22,6 @@ import graphql.language.Field import graphql.schema.DataFetcher import graphql.schema.GraphQLType import graphql.schema.GraphQLTypeUtil.isList -import java.util.concurrent.CompletableFuture import java.util.concurrent.ConcurrentHashMap /** @@ -143,13 +142,13 @@ class ExecutionBatchState { * @param field the [Field] that will transition to dispatched state. * @param fieldExecutionStrategyPath the [ResultPath] associated to the ExecutionStrategy that dispatched the [field]. * @param fieldGraphQLType the [GraphQLType] of the [field]. - * @param result the [DataFetcher] [CompletableFuture] result. + * @param result the [DataFetcher] polymorphic result, either a nullable materialized object or a CompletableFuture. */ fun fieldToDispatchedState( field: Field, fieldExecutionStrategyPath: ResultPath, fieldGraphQLType: GraphQLType, - result: CompletableFuture + result: Any? ) { val fieldExecutionStrategyPathString = fieldExecutionStrategyPath.toString() executionStrategiesState[fieldExecutionStrategyPathString]?.let { executionStrategyState -> diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/state/ExecutionStrategyState.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/state/ExecutionStrategyState.kt index 9b1d6b721e..d7cd9bc8aa 100644 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/state/ExecutionStrategyState.kt +++ b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/state/ExecutionStrategyState.kt @@ -108,20 +108,19 @@ class ExecutionStrategyState( * of the [Field] [DataFetcher] in order to calculate the [Field] [FieldFetchType]. * * @param graphQLType [GraphQLType] of the [Field] which [DataFetcher] was dispatched. - * @param result [CompletableFuture] result of the [DataFetcher.get] call. + * @param result the [DataFetcher] polymorphic result, either a nullable materialized object or a CompletableFuture. * @return this [FieldState]. */ fun toDispatchedState( graphQLType: GraphQLType, - result: CompletableFuture + result: Any? ): FieldState = this.also { this.fetchState = FieldFetchState.DISPATCHED this.graphQLType = graphQLType this.fetchType = when { - result.isDone -> FieldFetchType.SYNC - else -> FieldFetchType.ASYNC + result is CompletableFuture<*> -> FieldFetchType.ASYNC + else -> FieldFetchType.SYNC } - this@ExecutionStrategyState.dispatchedFields.updateAndGet { current -> current + 1 } } diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/state/SyncExecutionExhaustedState.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/state/SyncExecutionExhaustedState.kt index 14d1237585..17dc2b4875 100644 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/state/SyncExecutionExhaustedState.kt +++ b/executions/graphql-kotlin-dataloader-instrumentation/src/main/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/state/SyncExecutionExhaustedState.kt @@ -24,6 +24,7 @@ import graphql.GraphQLContext import graphql.execution.ExecutionId import graphql.execution.MergedField import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext +import graphql.execution.instrumentation.FieldFetchingInstrumentationContext import graphql.execution.instrumentation.InstrumentationContext import graphql.execution.instrumentation.SimpleInstrumentationContext import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters @@ -83,17 +84,16 @@ class SyncExecutionExhaustedState( } /** - * Add [ExecutionStrategyState] into operation [ExecutionBatchState] for the field that + * Add [ExecutionStrategyState] into operation [ExecutionBatchState] for the object that * just started an ExecutionStrategy * * @param parameters contains information of which [ExecutionInput] started an executionStrategy * @return a noop [ExecutionStrategyInstrumentationContext] */ - fun beginExecutionStrategy( + fun beginRecursiveExecution( parameters: InstrumentationExecutionStrategyParameters - ): ExecutionStrategyInstrumentationContext? { + ) { val executionId = parameters.executionContext.executionInput.executionId - executions.computeIfPresent(executionId) { _, executionState -> val executionStrategyParameters = parameters.executionStrategyParameters @@ -105,8 +105,6 @@ class SyncExecutionExhaustedState( executionState.addExecutionStrategyState(field, path, selectionFields, parentGraphQLType) executionState } - - return null } /** @@ -117,19 +115,19 @@ class SyncExecutionExhaustedState( * @return a [InstrumentationContext] object that will be called back when the [DataFetcher] * dispatches and completes */ - fun beginFieldFetch( + fun beginFieldFetching( parameters: InstrumentationFieldFetchParameters, onSyncExecutionExhausted: OnSyncExecutionExhaustedCallback - ): InstrumentationContext { + ): FieldFetchingInstrumentationContext { val executionId = parameters.executionContext.executionInput.executionId val field = parameters.executionStepInfo.field.singleField val fieldExecutionStrategyPath = parameters.executionStepInfo.path.parent val fieldGraphQLType = parameters.executionStepInfo.unwrappedNonNullType - return object : InstrumentationContext { - override fun onDispatched(result: CompletableFuture) { + return object : FieldFetchingInstrumentationContext { + override fun onFetchedValue(fetchedValue: Any?) { executions.computeIfPresent(executionId) { _, executionState -> - executionState.fieldToDispatchedState(field, fieldExecutionStrategyPath, fieldGraphQLType, result) + executionState.fieldToDispatchedState(field, fieldExecutionStrategyPath, fieldGraphQLType, fetchedValue) executionState } @@ -138,6 +136,7 @@ class SyncExecutionExhaustedState( onSyncExecutionExhausted(executions.keys().toList()) } } + override fun onCompleted(result: Any?, t: Throwable?) { executions.computeIfPresent(executionId) { _, executionState -> executionState.fieldToCompletedState(field, fieldExecutionStrategyPath, result) @@ -149,6 +148,9 @@ class SyncExecutionExhaustedState( onSyncExecutionExhausted(executions.keys().toList()) } } + + override fun onDispatched() { + } } } diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/fixture/AstronautGraphQL.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/fixture/AstronautGraphQL.kt index 4757c0dbf0..0d0ae4d06d 100644 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/fixture/AstronautGraphQL.kt +++ b/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/fixture/AstronautGraphQL.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Expedia, Inc + * Copyright 2024 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -32,7 +32,6 @@ import com.expediagroup.graphql.dataloader.instrumentation.fixture.datafetcher.P import com.expediagroup.graphql.dataloader.instrumentation.fixture.datafetcher.PlanetServiceRequest import com.expediagroup.graphql.dataloader.instrumentation.fixture.domain.Mission import com.expediagroup.graphql.dataloader.instrumentation.fixture.domain.Nasa -import com.expediagroup.graphql.dataloader.instrumentation.level.state.ExecutionLevelDispatchedState import com.expediagroup.graphql.dataloader.instrumentation.syncexhaustion.state.SyncExecutionExhaustedState import graphql.ExecutionInput import graphql.ExecutionResult @@ -217,10 +216,6 @@ object AstronautGraphQL { queries.size, kotlinDataLoaderRegistry ) - DataLoaderInstrumentationStrategy.LEVEL_DISPATCHED -> - ExecutionLevelDispatchedState::class to ExecutionLevelDispatchedState( - queries.size - ) } ) diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/fixture/DataLoaderInstrumentationStrategy.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/fixture/DataLoaderInstrumentationStrategy.kt index 36c85f7ebe..3bb4b43080 100644 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/fixture/DataLoaderInstrumentationStrategy.kt +++ b/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/fixture/DataLoaderInstrumentationStrategy.kt @@ -1,3 +1,3 @@ package com.expediagroup.graphql.dataloader.instrumentation.fixture -enum class DataLoaderInstrumentationStrategy { LEVEL_DISPATCHED, SYNC_EXHAUSTION } +enum class DataLoaderInstrumentationStrategy { SYNC_EXHAUSTION } diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/fixture/ProductGraphQL.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/fixture/ProductGraphQL.kt index 11149436b4..50aef25a12 100644 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/fixture/ProductGraphQL.kt +++ b/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/fixture/ProductGraphQL.kt @@ -24,7 +24,6 @@ import com.expediagroup.graphql.dataloader.instrumentation.fixture.datafetcher.P import com.expediagroup.graphql.dataloader.instrumentation.fixture.domain.Product import com.expediagroup.graphql.dataloader.instrumentation.fixture.domain.ProductDetails import com.expediagroup.graphql.dataloader.instrumentation.fixture.domain.ProductSummary -import com.expediagroup.graphql.dataloader.instrumentation.level.state.ExecutionLevelDispatchedState import com.expediagroup.graphql.dataloader.instrumentation.syncexhaustion.state.SyncExecutionExhaustedState import graphql.ExecutionInput import graphql.ExecutionResult @@ -125,10 +124,6 @@ object ProductGraphQL { queries.size, kotlinDataLoaderRegistry ) - DataLoaderInstrumentationStrategy.LEVEL_DISPATCHED -> - ExecutionLevelDispatchedState::class to ExecutionLevelDispatchedState( - queries.size - ) } ) diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/DataLoaderLevelDispatchedInstrumentationTest.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/DataLoaderLevelDispatchedInstrumentationTest.kt deleted file mode 100644 index 9fde9a5082..0000000000 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/DataLoaderLevelDispatchedInstrumentationTest.kt +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Copyright 2024 Expedia, Inc - * - * Licensed 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 - * - * https://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 com.expediagroup.graphql.dataloader.instrumentation.level - -import com.expediagroup.graphql.dataloader.instrumentation.fixture.AstronautGraphQL -import com.expediagroup.graphql.dataloader.instrumentation.fixture.DataLoaderInstrumentationStrategy -import io.mockk.clearAllMocks -import io.mockk.verify -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import kotlin.test.assertEquals - -class DataLoaderLevelDispatchedInstrumentationTest { - private val dataLoaderLevelDispatchedInstrumentation = DataLoaderLevelDispatchedInstrumentation() - private val graphQL = AstronautGraphQL.builder - .instrumentation(dataLoaderLevelDispatchedInstrumentation) - // graphql java adds DataLoaderDispatcherInstrumentation by default - .doNotAddDefaultInstrumentations() - .build() - - @BeforeEach - fun clear() { - clearAllMocks() - } - - @Test - fun `Instrumentation should batch transactions on async top level fields`() { - val queries = listOf( - "{ astronaut(id: 1) { name } }", - "{ astronaut(id: 2) { id name } }", - "{ mission(id: 3) { id designation } }", - "{ mission(id: 4) { designation } }" - ) - - val (results, kotlinDataLoaderRegistry) = AstronautGraphQL.execute( - graphQL, - queries, - DataLoaderInstrumentationStrategy.LEVEL_DISPATCHED - ) - - assertEquals(4, results.size) - - val astronautStatistics = kotlinDataLoaderRegistry.dataLoadersMap["AstronautDataLoader"]?.statistics - val missionStatistics = kotlinDataLoaderRegistry.dataLoadersMap["MissionDataLoader"]?.statistics - - assertEquals(1, astronautStatistics?.batchInvokeCount) - assertEquals(2, astronautStatistics?.batchLoadCount) - - assertEquals(1, missionStatistics?.batchInvokeCount) - assertEquals(2, missionStatistics?.batchLoadCount) - - verify(exactly = 2 + ONE_LEVEL) { - kotlinDataLoaderRegistry.dispatchAll() - } - } - - @Test - fun `Instrumentation should batch transactions on sync top level fields`() { - val queries = listOf( - "{ nasa { astronaut(id: 1) { name } } }", - "{ nasa { astronaut(id: 2) { id name } } }", - "{ nasa { mission(id: 3) { designation } } }", - "{ nasa { mission(id: 4) { id designation } } }" - ) - - val (results, kotlinDataLoaderRegistry) = AstronautGraphQL.execute( - graphQL, - queries, - DataLoaderInstrumentationStrategy.LEVEL_DISPATCHED - ) - - assertEquals(4, results.size) - - val astronautStatistics = kotlinDataLoaderRegistry.dataLoadersMap["AstronautDataLoader"]?.statistics - val missionStatistics = kotlinDataLoaderRegistry.dataLoadersMap["MissionDataLoader"]?.statistics - - assertEquals(1, astronautStatistics?.batchInvokeCount) - assertEquals(2, astronautStatistics?.batchLoadCount) - - assertEquals(1, missionStatistics?.batchInvokeCount) - assertEquals(2, missionStatistics?.batchLoadCount) - - verify(exactly = 3 + ONE_LEVEL) { - kotlinDataLoaderRegistry.dispatchAll() - } - } - - @Test - fun `Instrumentation should batch by level even if different levels attempt to use same dataFetchers`() { - val queries = listOf( - // L2 astronaut - L3 missions - "{ nasa { astronaut(id: 1) { id name missions { designation } } } }", - // L1 astronaut - L2 missions - "{ astronaut(id: 2) { id name missions { designation } } }", - // L2 mission - "{ nasa { mission(id: 3) { designation } } }", - // L1 mission - "{ mission(id: 4) { designation } }" - ) - - val (results, kotlinDataLoaderRegistry) = AstronautGraphQL.execute( - graphQL, - queries, - DataLoaderInstrumentationStrategy.LEVEL_DISPATCHED - ) - - assertEquals(4, results.size) - - val astronautStatistics = kotlinDataLoaderRegistry.dataLoadersMap["AstronautDataLoader"]?.statistics - val missionStatistics = kotlinDataLoaderRegistry.dataLoadersMap["MissionDataLoader"]?.statistics - val missionsByAstronautStatistics = kotlinDataLoaderRegistry.dataLoadersMap["MissionsByAstronautDataLoader"]?.statistics - - // 1 for Level 1, 1 for Level 2 - assertEquals(2, astronautStatistics?.batchInvokeCount) - assertEquals(2, astronautStatistics?.batchLoadCount) - - // 1 for Level 1, 1 for Level 2 - assertEquals(2, missionStatistics?.batchInvokeCount) - assertEquals(2, missionStatistics?.batchLoadCount) - - // 1 for Level 2, 1 for Level 3 - assertEquals(2, missionsByAstronautStatistics?.batchInvokeCount) - assertEquals(2, missionsByAstronautStatistics?.batchLoadCount) - - verify(exactly = 4 + ONE_LEVEL) { - kotlinDataLoaderRegistry.dispatchAll() - } - } - - @Test - fun `Instrumentation should not apply to mutations`() { - val queries = listOf( - """mutation { createAstronaut(name: "spaceMan") { id name } }""" - ) - - val (results, kotlinDataLoaderRegistry) = AstronautGraphQL.execute( - graphQL, - queries, - DataLoaderInstrumentationStrategy.LEVEL_DISPATCHED - ) - - assertEquals(1, results.size) - verify(exactly = 0) { - kotlinDataLoaderRegistry.dispatchAll() - } - } - - @Test - fun `Instrumentation should not account for invalid operations`() { - val queries = listOf( - "invalid query{ astronaut(id: 1) {", - "{ astronaut(id: 2) { id name } }", - "{ mission(id: 3) { id designation } }", - "{ mission(id: 4) { designation } }" - ) - - val (results, kotlinDataLoaderRegistry) = AstronautGraphQL.execute( - graphQL, - queries, - DataLoaderInstrumentationStrategy.LEVEL_DISPATCHED - ) - - assertEquals(4, results.size) - - val astronautStatistics = kotlinDataLoaderRegistry.dataLoadersMap["AstronautDataLoader"]?.statistics - val missionStatistics = kotlinDataLoaderRegistry.dataLoadersMap["MissionDataLoader"]?.statistics - - assertEquals(1, astronautStatistics?.batchInvokeCount) - assertEquals(1, astronautStatistics?.batchLoadCount) - - assertEquals(1, missionStatistics?.batchInvokeCount) - assertEquals(2, missionStatistics?.batchLoadCount) - - verify(exactly = 2 + ONE_LEVEL) { - kotlinDataLoaderRegistry.dispatchAll() - } - } - - companion object { - private const val ONE_LEVEL = 1 - } -} diff --git a/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/DataLoaderSyncExecutionExhaustedInstrumentationTest.kt b/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/DataLoaderSyncExecutionExhaustedInstrumentationTest.kt index 4147d49570..b6c4387f85 100644 --- a/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/DataLoaderSyncExecutionExhaustedInstrumentationTest.kt +++ b/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/syncexhaustion/DataLoaderSyncExecutionExhaustedInstrumentationTest.kt @@ -31,13 +31,13 @@ class DataLoaderSyncExecutionExhaustedInstrumentationTest { private val graphQL = AstronautGraphQL.builder .instrumentation(dataLoaderSyncExecutionExhaustedInstrumentation) // graphql java adds DataLoaderDispatcherInstrumentation by default - .doNotAddDefaultInstrumentations() + .doNotAutomaticallyDispatchDataLoader() .build() private val productGraphQL = ProductGraphQL.builder .instrumentation(DataLoaderSyncExecutionExhaustedInstrumentation()) // graphql java adds DataLoaderDispatcherInstrumentation by default - .doNotAddDefaultInstrumentations() + .doNotAutomaticallyDispatchDataLoader() .build() @BeforeEach @@ -569,7 +569,7 @@ class DataLoaderSyncExecutionExhaustedInstrumentationTest { val (results, dataLoaderSyncExecutionExhaustedInstrumentation) = AstronautGraphQL.execute( graphQL, queries, - DataLoaderInstrumentationStrategy.LEVEL_DISPATCHED + DataLoaderInstrumentationStrategy.SYNC_EXHAUSTION ) assertEquals(1, results.size) diff --git a/generator/graphql-kotlin-federation/build.gradle.kts b/generator/graphql-kotlin-federation/build.gradle.kts index 41fcab7d19..9cbe08ccd0 100644 --- a/generator/graphql-kotlin-federation/build.gradle.kts +++ b/generator/graphql-kotlin-federation/build.gradle.kts @@ -6,7 +6,10 @@ plugins { dependencies { api(projects.graphqlKotlinSchemaGenerator) - api(libs.federation) + api(libs.federation) { + exclude(group = "com.graphql-java", module = "graphql-java") + } + api(libs.graphql.java) testImplementation(libs.reactor.core) testImplementation(libs.reactor.extensions) testImplementation(libs.junit.params) diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/TestResolvers.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/TestResolvers.kt index df782a0043..9689b08533 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/TestResolvers.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/data/TestResolvers.kt @@ -27,7 +27,7 @@ import java.util.concurrent.CompletableFuture internal class BookResolver : FederatedTypeSuspendResolver { override val typeName: String = "Book" - override suspend fun resolve(environment: DataFetchingEnvironment, representation: Map): Book? { + override suspend fun resolve(environment: DataFetchingEnvironment, representation: Map): Book { val book = Book(representation["id"].toString()) representation["weight"]?.toString()?.toDoubleOrNull()?.let { book.weight = it @@ -39,7 +39,7 @@ internal class BookResolver : FederatedTypeSuspendResolver { internal class UserResolver : FederatedTypeSuspendResolver { override val typeName: String = "User" - override suspend fun resolve(environment: DataFetchingEnvironment, representation: Map): User? { + override suspend fun resolve(environment: DataFetchingEnvironment, representation: Map): User { val id = representation["userId"].toString().toInt() val name = representation["name"].toString() return User(id, name) diff --git a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/ServiceQueryResolverTest.kt b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/ServiceQueryResolverTest.kt index 94c67493c9..522e0e68d4 100644 --- a/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/ServiceQueryResolverTest.kt +++ b/generator/graphql-kotlin-federation/src/test/kotlin/com/expediagroup/graphql/generator/federation/execution/ServiceQueryResolverTest.kt @@ -71,7 +71,7 @@ type Query @extends type Review { body: String! - content: String @deprecated(reason : "no longer supported, replace with use Review.body instead") + content: String customScalar: CustomScalar! id: String! } @@ -173,7 +173,7 @@ type Query { type Review { body: String! @custom - content: String @deprecated(reason : "no longer supported, replace with use Review.body instead") + content: String customScalar: CustomScalar! id: String! } diff --git a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/FlowSubscriptionExecutionStrategy.kt b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/FlowSubscriptionExecutionStrategy.kt index 28ef374b61..61146a197f 100644 --- a/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/FlowSubscriptionExecutionStrategy.kt +++ b/generator/graphql-kotlin-schema-generator/src/main/kotlin/com/expediagroup/graphql/generator/execution/FlowSubscriptionExecutionStrategy.kt @@ -1,5 +1,5 @@ /* - * Copyright 2022 Expedia, Inc + * Copyright 2024 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import graphql.execution.ExecutionContext import graphql.execution.ExecutionStepInfo import graphql.execution.ExecutionStrategy import graphql.execution.ExecutionStrategyParameters +import graphql.execution.FetchedValue import graphql.execution.SimpleDataFetcherExceptionHandler import graphql.execution.SubscriptionExecutionStrategy import graphql.execution.instrumentation.ExecutionStrategyInstrumentationContext @@ -84,7 +85,7 @@ class FlowSubscriptionExecutionStrategy(dfe: DataFetcherExceptionHandler) : Exec } // dispatched the subscription query - executionStrategyCtx.onDispatched(overallResult) + executionStrategyCtx.onDispatched() overallResult.whenComplete(executionStrategyCtx::onCompleted) return overallResult @@ -103,13 +104,20 @@ class FlowSubscriptionExecutionStrategy(dfe: DataFetcherExceptionHandler) : Exec Let {fieldStream} be the result of running {ResolveFieldEventStream(subscriptionType, initialValue, rootField, argumentValues)}. Return {fieldStream}. */ + @Suppress("UNCHECKED_CAST") private fun createSourceEventStream( executionContext: ExecutionContext, parameters: ExecutionStrategyParameters ): CompletableFuture?> { val newParameters = firstFieldOfSubscriptionSelection(parameters) - val fieldFetched = fetchField(executionContext, newParameters) + val fieldFetched: CompletableFuture = fetchField(executionContext, newParameters).let { fetchedValue -> + if (fetchedValue is CompletableFuture<*>) { + fetchedValue as CompletableFuture + } else { + CompletableFuture.completedFuture(fetchedValue as FetchedValue) + } + } return fieldFetched.thenApply { fetchedValue -> val flow = when (val publisherOrFlow: Any? = fetchedValue.fetchedValue) { is Publisher<*> -> publisherOrFlow.asFlow() @@ -163,12 +171,12 @@ class FlowSubscriptionExecutionStrategy(dfe: DataFetcherExceptionHandler) : Exec .thenApply { executionResult -> wrapWithRootFieldName(newParameters, executionResult) } // dispatch instrumentation so they can know about each subscription event - subscribedFieldCtx.onDispatched(overallResult) + subscribedFieldCtx.onDispatched() overallResult.whenComplete(subscribedFieldCtx::onCompleted) // allow them to instrument each ER should they want to val i13nExecutionParameters = InstrumentationExecutionParameters( - executionContext.executionInput, executionContext.graphQLSchema, executionContext.instrumentationState + executionContext.executionInput, executionContext.graphQLSchema ) return overallResult.thenCompose { executionResult -> diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5da1198082..2835826d59 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,9 +1,9 @@ [versions] android-plugin = "8.1.1" classgraph = "4.8.170" -dataloader = "3.2.2" +dataloader = "3.3.0" federation = "4.4.1" -graphql-java = "21.5" +graphql-java = "0.0.0-2024-04-23-expedia-branch-1" graalvm = "0.10.1" jackson = "2.15.4" # kotlin version has to match the compile-testing compiler version diff --git a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGenerator.kt b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGenerator.kt index e4b02c55ec..50ac01f991 100644 --- a/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGenerator.kt +++ b/plugins/client/graphql-kotlin-client-generator/src/main/kotlin/com/expediagroup/graphql/plugin/client/generator/GraphQLClientGenerator.kt @@ -33,6 +33,7 @@ import com.squareup.kotlinpoet.TypeSpec import graphql.language.ObjectTypeDefinition import graphql.language.OperationDefinition import graphql.parser.Parser +import graphql.parser.ParserEnvironment import graphql.parser.ParserOptions import graphql.schema.idl.SchemaParser import graphql.schema.idl.TypeDefinitionRegistry @@ -94,9 +95,14 @@ class GraphQLClientGenerator( */ internal fun generate(queryFile: File): List { val queryConst = queryFile.readText().trim() - val queryDocument = documentParser.parseDocument(queryConst, parserOptions) + val queryDocument = documentParser.parseDocument( + ParserEnvironment.newParserEnvironment() + .document(queryConst) + .parserOptions(parserOptions) + .build() + ) - val operationDefinitions = queryDocument.definitions.filterIsInstance(OperationDefinition::class.java) + val operationDefinitions = queryDocument.definitions.filterIsInstance() if (operationDefinitions.size > 1) { throw MultipleOperationsInFileException(queryFile) } diff --git a/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQL.kt b/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQL.kt index bc6693f7ac..93e324ab58 100644 --- a/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQL.kt +++ b/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQL.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Expedia, Inc + * Copyright 2024 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ package com.expediagroup.graphql.server.ktor import com.apollographql.federation.graphqljava.tracing.FederatedTracingInstrumentation import com.expediagroup.graphql.apq.provider.AutomaticPersistedQueriesProvider -import com.expediagroup.graphql.dataloader.instrumentation.level.DataLoaderLevelDispatchedInstrumentation import com.expediagroup.graphql.dataloader.instrumentation.syncexhaustion.DataLoaderSyncExecutionExhaustedInstrumentation import com.expediagroup.graphql.generator.ClasspathTypeResolver import com.expediagroup.graphql.generator.SchemaGenerator @@ -135,10 +134,9 @@ class GraphQL(config: GraphQLConfiguration) { val instrumentations = mutableListOf() if (config.engine.batching.enabled) { - builder.doNotAddDefaultInstrumentations() + builder.doNotAutomaticallyDispatchDataLoader() instrumentations.add( when (config.engine.batching.strategy) { - GraphQLConfiguration.BatchingStrategy.LEVEL_DISPATCHED -> DataLoaderLevelDispatchedInstrumentation() GraphQLConfiguration.BatchingStrategy.SYNC_EXHAUSTION -> DataLoaderSyncExecutionExhaustedInstrumentation() } ) diff --git a/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQLConfiguration.kt b/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQLConfiguration.kt index 36bb50954f..077dab9079 100644 --- a/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQLConfiguration.kt +++ b/servers/graphql-kotlin-ktor-server/src/main/kotlin/com/expediagroup/graphql/server/ktor/GraphQLConfiguration.kt @@ -235,7 +235,7 @@ class GraphQLConfiguration(config: ApplicationConfig) { /** * Approaches for batching transactions of a set of GraphQL Operations. */ - enum class BatchingStrategy { LEVEL_DISPATCHED, SYNC_EXHAUSTION } + enum class BatchingStrategy { SYNC_EXHAUSTION } /** * Batching configuration properties. @@ -288,4 +288,4 @@ class GraphQLConfiguration(config: ApplicationConfig) { } private fun String?.toBatchingStrategy(): GraphQLConfiguration.BatchingStrategy = - GraphQLConfiguration.BatchingStrategy.values().firstOrNull { strategy -> strategy.name == this } ?: GraphQLConfiguration.BatchingStrategy.LEVEL_DISPATCHED + GraphQLConfiguration.BatchingStrategy.values().firstOrNull { strategy -> strategy.name == this } ?: GraphQLConfiguration.BatchingStrategy.SYNC_EXHAUSTION diff --git a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/execution/GraphQLRequestHandler.kt b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/execution/GraphQLRequestHandler.kt index 6808d1021e..d0a2e7209d 100644 --- a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/execution/GraphQLRequestHandler.kt +++ b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/execution/GraphQLRequestHandler.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Expedia, Inc + * Copyright 2024 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,6 @@ package com.expediagroup.graphql.server.execution import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistry import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory -import com.expediagroup.graphql.dataloader.instrumentation.level.DataLoaderLevelDispatchedInstrumentation -import com.expediagroup.graphql.dataloader.instrumentation.level.state.ExecutionLevelDispatchedState import com.expediagroup.graphql.dataloader.instrumentation.syncexhaustion.DataLoaderSyncExecutionExhaustedInstrumentation import com.expediagroup.graphql.dataloader.instrumentation.syncexhaustion.state.SyncExecutionExhaustedState import com.expediagroup.graphql.generator.extensions.plus @@ -140,22 +138,13 @@ open class GraphQLRequestHandler( batchSize: Int, dataLoaderRegistry: KotlinDataLoaderRegistry? ): Map<*, Any> { - if (dataLoaderRegistry == null) { - return emptyMap() - } - - val batchContext = when (batchDataLoaderInstrumentationType) { - DataLoaderLevelDispatchedInstrumentation::class.java -> mapOf( - ExecutionLevelDispatchedState::class to ExecutionLevelDispatchedState(batchSize) - ) - + dataLoaderRegistry ?: return emptyMap() + return when (batchDataLoaderInstrumentationType) { DataLoaderSyncExecutionExhaustedInstrumentation::class.java -> mapOf( SyncExecutionExhaustedState::class to SyncExecutionExhaustedState(batchSize, dataLoaderRegistry) ) - else -> emptyMap() } - return batchContext } /** diff --git a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/extensions/instrumentationExtensions.kt b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/extensions/instrumentationExtensions.kt index eacc486174..087554227b 100644 --- a/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/extensions/instrumentationExtensions.kt +++ b/servers/graphql-kotlin-server/src/main/kotlin/com/expediagroup/graphql/server/extensions/instrumentationExtensions.kt @@ -1,9 +1,7 @@ package com.expediagroup.graphql.server.extensions -import com.expediagroup.graphql.dataloader.instrumentation.level.DataLoaderLevelDispatchedInstrumentation import com.expediagroup.graphql.dataloader.instrumentation.syncexhaustion.DataLoaderSyncExecutionExhaustedInstrumentation import graphql.execution.instrumentation.Instrumentation internal fun Instrumentation.isBatchDataLoaderInstrumentation(): Boolean = - javaClass == DataLoaderLevelDispatchedInstrumentation::class.java || - javaClass == DataLoaderSyncExecutionExhaustedInstrumentation::class.java + javaClass == DataLoaderSyncExecutionExhaustedInstrumentation::class.java diff --git a/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/execution/GraphQLRequestHandlerTest.kt b/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/execution/GraphQLRequestHandlerTest.kt index b666b8ba33..b2813c32b6 100644 --- a/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/execution/GraphQLRequestHandlerTest.kt +++ b/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/execution/GraphQLRequestHandlerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Expedia, Inc + * Copyright 2024 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,6 @@ package com.expediagroup.graphql.server.execution import com.expediagroup.graphql.dataloader.KotlinDataLoader import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory -import com.expediagroup.graphql.dataloader.instrumentation.level.DataLoaderLevelDispatchedInstrumentation import com.expediagroup.graphql.dataloader.instrumentation.syncexhaustion.DataLoaderSyncExecutionExhaustedInstrumentation import com.expediagroup.graphql.generator.SchemaGeneratorConfig import com.expediagroup.graphql.generator.TopLevelObject @@ -71,7 +70,7 @@ class GraphQLRequestHandlerTest { private fun getBatchingRequestHandler(instrumentation: Instrumentation): GraphQLRequestHandler = GraphQLRequestHandler( - GraphQL.newGraphQL(testSchema).doNotAddDefaultInstrumentations().instrumentation(instrumentation).build(), + GraphQL.newGraphQL(testSchema).doNotAutomaticallyDispatchDataLoader().instrumentation(instrumentation).build(), KotlinDataLoaderRegistryFactory( object : KotlinDataLoader { override val dataLoaderName: String = "UserDataLoader" @@ -303,7 +302,7 @@ class GraphQLRequestHandlerTest { ) val batchingRequestHandler = getBatchingRequestHandler( ChainedInstrumentation( - DataLoaderLevelDispatchedInstrumentation() + DataLoaderSyncExecutionExhaustedInstrumentation() ) ) batchingRequestHandler.executeRequest(request) as GraphQLBatchResponse @@ -333,7 +332,7 @@ class GraphQLRequestHandlerTest { val response = runBlocking { val context = emptyMap().toGraphQLContext() val request = GraphQLRequest( - query = "subscription { users(name: \"Jane Doe\") { name } }", + query = """subscription { users(name: "Jane Doe") { name } }""", ) graphQLRequestHandler.executeSubscription(request, context).first() } diff --git a/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/execution/subscription/GraphQLWebSocketServerTest.kt b/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/execution/subscription/GraphQLWebSocketServerTest.kt index 93beba186a..ac4159c879 100644 --- a/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/execution/subscription/GraphQLWebSocketServerTest.kt +++ b/servers/graphql-kotlin-server/src/test/kotlin/com/expediagroup/graphql/server/execution/subscription/GraphQLWebSocketServerTest.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Expedia, Inc + * Copyright 2024 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,7 +39,6 @@ import graphql.GraphQL import io.mockk.coEvery import io.mockk.coVerify import io.mockk.spyk -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay @@ -55,7 +54,6 @@ import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue -@OptIn(ExperimentalCoroutinesApi::class) class GraphQLWebSocketServerTest { private val mapper = jacksonObjectMapper() diff --git a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLConfigurationProperties.kt b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLConfigurationProperties.kt index 7fbbc90fdf..91d467de2c 100644 --- a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLConfigurationProperties.kt +++ b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLConfigurationProperties.kt @@ -143,7 +143,7 @@ data class GraphQLConfigurationProperties( /** Boolean flag to enable or disable batching for a set of GraphQL Operations. */ val enabled: Boolean = false, /** configure the [BatchingStrategy] that will be used when batching is enabled for a set of GraphQL Operations. */ - val strategy: BatchingStrategy = BatchingStrategy.LEVEL_DISPATCHED + val strategy: BatchingStrategy = BatchingStrategy.SYNC_EXHAUSTION ) data class AutomaticPersistedQueriesConfigurationProperties( @@ -164,6 +164,5 @@ enum class SubscriptionProtocol { * Approaches for batching transactions of a set of GraphQL Operations. */ enum class BatchingStrategy { - LEVEL_DISPATCHED, SYNC_EXHAUSTION } diff --git a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLSchemaConfiguration.kt b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLSchemaConfiguration.kt index d35600f702..b7914d7928 100644 --- a/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLSchemaConfiguration.kt +++ b/servers/graphql-kotlin-spring-server/src/main/kotlin/com/expediagroup/graphql/server/spring/GraphQLSchemaConfiguration.kt @@ -1,5 +1,5 @@ /* - * Copyright 2023 Expedia, Inc + * Copyright 2024 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import com.expediagroup.graphql.apq.provider.AutomaticPersistedQueriesProvider import com.expediagroup.graphql.generator.execution.FlowSubscriptionExecutionStrategy import com.expediagroup.graphql.generator.scalars.IDValueUnboxer import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory -import com.expediagroup.graphql.dataloader.instrumentation.level.DataLoaderLevelDispatchedInstrumentation import com.expediagroup.graphql.dataloader.instrumentation.syncexhaustion.DataLoaderSyncExecutionExhaustedInstrumentation import com.expediagroup.graphql.server.execution.GraphQLRequestHandler import com.expediagroup.graphql.server.spring.execution.DefaultSpringGraphQLContextFactory @@ -88,10 +87,9 @@ class GraphQLSchemaConfiguration { val instrumentations = mutableListOf() if (config.batching.enabled) { - builder.doNotAddDefaultInstrumentations() + builder.doNotAutomaticallyDispatchDataLoader() instrumentations.add( when (config.batching.strategy) { - BatchingStrategy.LEVEL_DISPATCHED -> DataLoaderLevelDispatchedInstrumentation() BatchingStrategy.SYNC_EXHAUSTION -> DataLoaderSyncExecutionExhaustedInstrumentation() } ) diff --git a/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/instrumentation/InstrumentationIT.kt b/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/instrumentation/InstrumentationIT.kt index 60267d3a90..0fbb71cc12 100644 --- a/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/instrumentation/InstrumentationIT.kt +++ b/servers/graphql-kotlin-spring-server/src/test/kotlin/com/expediagroup/graphql/server/spring/instrumentation/InstrumentationIT.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020 Expedia, Inc + * Copyright 2024 Expedia, Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,8 @@ import com.expediagroup.graphql.server.types.GraphQLRequest import graphql.ExecutionResult import graphql.ExecutionResultImpl import graphql.execution.instrumentation.Instrumentation -import graphql.execution.instrumentation.SimpleInstrumentation +import graphql.execution.instrumentation.InstrumentationState +import graphql.execution.instrumentation.SimplePerformantInstrumentation import graphql.execution.instrumentation.parameters.InstrumentationExecutionParameters import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -59,8 +60,8 @@ class InstrumentationIT(@Autowired private val testClient: WebTestClient) { fun helloWorld(name: String) = "Hello $name!" } - class OrderedInstrumentation(private val instrumentationOrder: Int, private val counter: AtomicInteger) : SimpleInstrumentation(), Ordered { - override fun instrumentExecutionResult(executionResult: ExecutionResult, parameters: InstrumentationExecutionParameters): CompletableFuture { + class OrderedInstrumentation(private val instrumentationOrder: Int, private val counter: AtomicInteger) : SimplePerformantInstrumentation(), Ordered { + override fun instrumentExecutionResult(executionResult: ExecutionResult, parameters: InstrumentationExecutionParameters, state: InstrumentationState?): CompletableFuture { val currentExt: MutableMap = executionResult.extensions?.toMutableMap() ?: mutableMapOf() currentExt[instrumentationOrder] = counter.getAndIncrement() @@ -76,7 +77,7 @@ class InstrumentationIT(@Autowired private val testClient: WebTestClient) { .uri("/graphql") .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) - .bodyValue(GraphQLRequest("query { helloWorld(name: \"World\") }")) + .bodyValue(GraphQLRequest("""query { helloWorld(name: "World") }""")) .exchange() .expectBody() .jsonPath("$.data.helloWorld").isEqualTo("Hello World!") diff --git a/website/docs/assets/data-loader-level-dispatched-instrumentation.png b/website/docs/assets/data-loader-level-dispatched-instrumentation.png deleted file mode 100644 index a83d8150da..0000000000 Binary files a/website/docs/assets/data-loader-level-dispatched-instrumentation.png and /dev/null differ diff --git a/website/docs/server/data-loader/data-loader-instrumentation.mdx b/website/docs/server/data-loader/data-loader-instrumentation.mdx index 0aca14ec53..6568aa8a8b 100644 --- a/website/docs/server/data-loader/data-loader-instrumentation.mdx +++ b/website/docs/server/data-loader/data-loader-instrumentation.mdx @@ -18,100 +18,7 @@ allowing batching and deduplication of transactions across those multiple GraphQ By default, each GraphQL operation is processed independently of each other. Multiple operations can be processed together as if they were single GraphQL request if they are part of the same batch request. -The `graphql-kotlin-dataloader-instrumentation` module contains 2 custom `DataLoader` instrumentations. - -## Dispatching by level - -The `DataLoaderLevelDispatchedInstrumentation` tracks the state of all `ExecutionInputs` across operations. When a certain -field dispatches, it will check if all fields across all operations for a particular level were dispatched and if the condition is met, -it will dispatch all the data loaders. - -### Example - -You can find additional examples in our [unit tests](https://github.com/ExpediaGroup/graphql-kotlin/blob/master/executions/graphql-kotlin-dataloader-instrumentation/src/test/kotlin/com/expediagroup/graphql/dataloader/instrumentation/level/DataLoaderLevelDispatchedInstrumentationTest.kt). - - - - - -```graphql -query Q1 { - astronaut(id: 1) { # async - id - name - missions { # async - id - designation - } - } -} - -query Q2 { - astronaut(id: 2) { # async - id - name - missions { # async - id - designation - } - } -} -``` - - - - -![Image of data loader level dispatched instrumentation](../../assets/data-loader-level-dispatched-instrumentation.png) - -* The `astronaut` `DataFetcher` uses a `AstronautDataLoader` which will be dispatched when **Level 1** of those 2 operations -is dispatched, causing the `AstronautDataLoader` to load 2 astronauts. -* The `missions` `DataFetcher` uses a `MissionsByAstronautDataLoader` which will be dispatched when **Level 2** of those 2 operations -is dispatched, causing the `MissionsByAstronautDataLoader` to load 2 lists of missions by astronaut. - - - - -### Usage - -In order to enable batching by level, you need to configure your GraphQL instance with the `DataLoaderLevelDispatchedInstrumentation`. - -```kotlin -val graphQL = GraphQL.Builder() - .doNotAddDefaultInstrumentations() - .instrumentation(DataLoaderLevelDispatchedInstrumentation()) - // configure schema, type wiring, etc. - .build() -``` - -This data loader instrumentation relies on a global state object that needs be stored in the GraphQLContext map - -```kotlin -val graphQLContext = mapOf( - ExecutionLevelDispatchedState::class to ExecutionLevelDispatchedState(queries.size) -) -``` - -:::info -`graphql-kotlin-spring-server` provides convenient integration of batch loader functionality through simple configuration. -Batching by level can be enabled by configuring following properties: -```yaml -graphql: - batching: - enabled: true - strategy: LEVEL_DISPATCHED -``` -::: - -### Limitations - -This instrumentation is a good option if your **GraphQLServer** will receive a batched request with operations of the same type, -in those cases batching by level is enough, however, this solution is far from being the most optimal as we don't necessarily want to dispatch by level. +The `graphql-kotlin-dataloader-instrumentation` module contains 1 custom `DataLoader` instrumentation. ## Dispatching by synchronous execution exhaustion diff --git a/website/docs/server/spring-server/spring-properties.md b/website/docs/server/spring-server/spring-properties.md index 9ac6e3bec0..717e9f3ffa 100644 --- a/website/docs/server/spring-server/spring-properties.md +++ b/website/docs/server/spring-server/spring-properties.md @@ -31,4 +31,4 @@ details on the supported configuration properties. | graphql.subscriptions.keepAliveInterval | **Deprecated**. Keep the websocket alive and send a message to the client every interval in ms. Defaults to not sending messages | null | | graphql.subscriptions.protocol | WebSocket based subscription protocol. Supported protocols: APOLLO_SUBSCRIPTIONS_WS / GRAPHQL_WS | GRAPHQL_WS | | graphql.batching.enabled | Boolean flag indicating whether to enable custom dataloader instrumentations for 1 or more GraphQL Operations | false | -| graphql.batching.strategy | Configure which custom dataloader instrumentation will be used (LEVEL_DISPATCHED or SYNC_EXHAUSTION) | LEVEL_DISPATCHED | +| graphql.batching.strategy | Configure which custom dataloader instrumentation will be used | SYNC_EXHAUSTION |