From 490709f26a8c982a55582e4f105be2daf2019c7c Mon Sep 17 00:00:00 2001 From: Dariusz Kuc Date: Mon, 29 Jun 2020 14:16:31 -0500 Subject: [PATCH] [plugin] expose timeout config for downloadSDL/introspectSchema tasks (#775) * [plugin] expose timeout configuration for downloadSDL/introspectSchema tasks Expose new read/connect timeout configuration for downloadSDL and introspectSchema tasks (and corresponding MOJOs). Example configuration: ```kotlin graphql { client { endpoint = "http://localhost:8080/graphql" packageName = "com.example.generated" timeout { // Connect timeout in milliseconds connect = 5_000 // Read timeout in milliseconds read = 15_000 } } } ``` ```xml com.expediagroup graphql-kotlin-maven-plugin ${graphql-kotlin.version} introspect-schema http://localhost:8080/graphql 5000 15000 ``` Resolves: https://github.com/ExpediaGroup/graphql-kotlin/issues/745 * update timeout values for test It looks like GH Actions sometimes take a bit longer to execute so bumping up the response delay from 1s to 10s. * disable parallel maven integration tests * enable streaming mvn logs to std out * unique integration test maven project names --- docs/plugins/gradle-plugin.md | 18 +++ docs/plugins/maven-plugin.md | 37 ++++- .../graphql-kotlin-gradle-plugin/README.md | 8 + .../plugin/gradle/GraphQLGradlePlugin.kt | 2 + .../plugin/gradle/GraphQLPluginExtension.kt | 8 + .../gradle/tasks/GraphQLDownloadSDLTask.kt | 11 +- .../tasks/GraphQLIntrospectSchemaTask.kt | 11 +- .../plugin/gradle/GraphQLDownloadSDLTaskIT.kt | 25 +++ .../gradle/GraphQLGradlePluginAbstractIT.kt | 16 +- .../gradle/GraphQLIntrospectSchemaTaskIT.kt | 25 +++ plugins/graphql-kotlin-maven-plugin/README.md | 38 ++++- plugins/graphql-kotlin-maven-plugin/pom.xml | 1 + .../download-sdl-timeout/invoker.properties | 1 + .../integration/download-sdl-timeout/pom.xml | 142 +++++++++++++++++ .../integration/generate-test-client/pom.xml | 2 +- .../introspect-timeout/invoker.properties | 1 + .../integration/introspect-timeout/pom.xml | 146 ++++++++++++++++++ .../mappings/introspectionTimeout.json | 22 +++ .../wiremock/mappings/sdlTimeout.json | 14 ++ .../graphql/plugin/maven/DownloadSDLMojo.kt | 36 +---- .../plugin/maven/IntrospectSchemaMojo.kt | 37 +---- .../maven/RetrieveSchemaAbstractMojo.kt | 84 ++++++++++ .../graphql/plugin/config/TimeoutConfig.kt | 13 ++ .../graphql/plugin/downloadSchema.kt | 18 ++- .../graphql/plugin/introspectSchema.kt | 19 ++- .../graphql/plugin/DownloadSchemaTest.kt | 37 ++++- .../graphql/plugin/IntrospectSchemaTest.kt | 25 ++- 27 files changed, 703 insertions(+), 94 deletions(-) create mode 100644 plugins/graphql-kotlin-maven-plugin/src/integration/download-sdl-timeout/invoker.properties create mode 100755 plugins/graphql-kotlin-maven-plugin/src/integration/download-sdl-timeout/pom.xml create mode 100644 plugins/graphql-kotlin-maven-plugin/src/integration/introspect-timeout/invoker.properties create mode 100755 plugins/graphql-kotlin-maven-plugin/src/integration/introspect-timeout/pom.xml create mode 100755 plugins/graphql-kotlin-maven-plugin/src/integration/wiremock/mappings/introspectionTimeout.json create mode 100755 plugins/graphql-kotlin-maven-plugin/src/integration/wiremock/mappings/sdlTimeout.json create mode 100644 plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/RetrieveSchemaAbstractMojo.kt create mode 100644 plugins/graphql-kotlin-plugin-core/src/main/kotlin/com/expediagroup/graphql/plugin/config/TimeoutConfig.kt diff --git a/docs/plugins/gradle-plugin.md b/docs/plugins/gradle-plugin.md index 01700d0ea0..09ec2645ca 100644 --- a/docs/plugins/gradle-plugin.md +++ b/docs/plugins/gradle-plugin.md @@ -46,6 +46,13 @@ graphql { converters["UUID"] = ScalarConverterMapping("java.util.UUID", "com.example.UUIDScalarConverter") // List of query files to be processed. queryFiles.add(file("${project.projectDir}/src/main/resources/queries/MyQuery.graphql")) + // Timeout configuration + timeout { + // Connect timeout in milliseconds + connect = 5_000 + // Read timeout in milliseconds + read = 15_000 + } } } ``` @@ -68,6 +75,7 @@ and could be used as an alternative to `graphqlIntrospectSchema` to generate inp | -------- | ---- | -------- | ----------- | | `endpoint` | String | yes | Target GraphQL server SDL endpoint that will be used to download schema.
**Command line property is**: `endpoint`. | | `headers` | Map | | Optional HTTP headers to be specified on a SDL request. | +| `timeoutConfig` | TimeoutConfig | | Timeout configuration(in milliseconds) to download schema from SDL endpoint before we cancel the request.
**Default value are:** connect timeout = 5_000, read timeout = 15_000.
| ### graphqlGenerateClient @@ -121,6 +129,7 @@ should be used to generate input for the subsequent `graphqlGenerateClient` task | -------- | ---- | -------- | ----------- | | `endpoint` | String | yes | Target GraphQL server endpoint that will be used to execute introspection queries.
**Command line property is**: `endpoint`. | | `headers` | Map | | Optional HTTP headers to be specified on an introspection query. | +| `timeoutConfig` | TimeoutConfig | | Timeout configuration(in milliseconds) to download schema from SDL endpoint before we cancel the request.
**Default value are:** connect timeout = 5_000, read timeout = 15_000.
| ## Examples @@ -307,6 +316,8 @@ the GraphQL client code based on the provided query. ```kotlin // build.gradle.kts +import com.expediagroup.graphql.plugin.config.TimeoutConfig +import com.expediagroup.graphql.plugin.generator.ScalarConverterMapping import com.expediagroup.graphql.plugin.gradle.graphql graphql { @@ -318,6 +329,10 @@ graphql { headers["X-Custom-Header"] = "My-Custom-Header" converters["UUID"] = ScalarConverterMapping("java.util.UUID", "com.example.UUIDScalarConverter") queryFiles.add(file("${project.projectDir}/src/main/resources/queries/MyQuery.graphql")) + timeout { + connect = 10_000 + read = 30_000 + } } } ``` @@ -326,12 +341,15 @@ Above configuration is equivalent to the following ```kotlin // build.gradle.kts +import com.expediagroup.graphql.plugin.config.TimeoutConfig +import com.expediagroup.graphql.plugin.generator.ScalarConverterMapping import com.expediagroup.graphql.plugin.gradle.tasks.GraphQLDownloadSDLTask import com.expediagroup.graphql.plugin.gradle.tasks.GraphQLIntrospectSchemaTask val graphqlDownloadSDL by tasks.getting(GraphQLDownloadSDLTask::class) { endpoint.set("http://localhost:8080/sdl") headers.put("X-Custom-Header", "My-Custom-Header") + timeoutConfig.set(TimeoutConfig(connect = 10_000, read = 30_000)) } val graphqlGenerateClient by tasks.getting(GraphQLGenerateClientTask::class) { packageName.set("com.example.generated") diff --git a/docs/plugins/maven-plugin.md b/docs/plugins/maven-plugin.md index 4122b1d7aa..d95c906b4b 100644 --- a/docs/plugins/maven-plugin.md +++ b/docs/plugins/maven-plugin.md @@ -29,6 +29,19 @@ goal provides limited functionality by itself and instead should be used to gene | -------- | ---- | -------- | ----------- | | `endpoint` | String | yes | Target GraphQL server SDL endpoint that will be used to download schema.
**User property is**: `graphql.endpoint`. | | `headers` | Map | | Optional HTTP headers to be specified on a SDL request. +| `timeoutConfiguration` | TimeoutConfiguration | | Optional timeout configuration (in milliseconds) to download schema from SDL endpoint before we cancel the request.
**Default values are:** connect timeout = 5000, read timeout = 15000.
| + +**Parameter Details** + + * *timeoutConfiguration* - Timeout configuration that allows you to specify connect and read timeout values in milliseconds. + + ```xml + + + 1000 + 30000 + + ``` ### generate-client @@ -124,6 +137,19 @@ should be used to generate input for the subsequent `generate-client` goal. | -------- | ---- | -------- | ----------- | | `endpoint` | String | yes | Target GraphQL server endpoint that will be used to execute introspection queries.
**User property is**: `graphql.endpoint`. | | `headers` | Map | | Optional HTTP headers to be specified on an introspection query. | +| `timeoutConfiguration` | TimeoutConfiguration | | Optional timeout configuration(in milliseconds) to execute introspection query before we cancel the request.
**Default values are:** connect timeout = 5000, read timeout = 15000.
| + +**Parameter Details** + + * *timeoutConfiguration* - Timeout configuration that allows you to specify connect and read timeout values in milliseconds. + + ```xml + + + 1000 + 30000 + + ``` ## Examples @@ -374,9 +400,6 @@ the GraphQL client code based on the provided query. ${project.build.directory}/schema.graphql true - - My-Custom-Header - @@ -387,6 +410,14 @@ the GraphQL client code based on the provided query. com.example.UUIDScalarConverter + + My-Custom-Header + + + + 1000 + 30000 + ${project.basedir}/src/main/resources/queries/MyQuery.graphql diff --git a/plugins/graphql-kotlin-gradle-plugin/README.md b/plugins/graphql-kotlin-gradle-plugin/README.md index b70f6ee16b..dbc3231bb9 100755 --- a/plugins/graphql-kotlin-gradle-plugin/README.md +++ b/plugins/graphql-kotlin-gradle-plugin/README.md @@ -38,6 +38,12 @@ graphql { converters["UUID"] = ScalarConverterMapping("java.util.UUID", "com.example.UUIDScalarConverter") // List of query files to be processed. queryFiles.add(file("${project.projectDir}/src/main/resources/queries/MyQuery.graphql")) + timeout { + // Connect timeout in milliseconds + connect = 5_000 + // Read timeout in milliseconds + read = 15_000 + } } } ``` @@ -61,6 +67,7 @@ and could be used as an alternative to `graphqlIntrospectSchema` to generate inp | -------- | ---- | -------- | ----------- | | `endpoint` | String | yes | Target GraphQL server SDL endpoint that will be used to download schema.
**Command line property is**: `endpoint`. | | `headers` | Map | | Optional HTTP headers to be specified on a SDL request. | +| `timeoutConfig` | TimeoutConfig | | Optional timeout configuration(in milliseconds) to download schema from SDL endpoint before we cancel the request.
**Default value are:** connect timeout = 5_000, read timeout = 15_000.
| ### graphqlGenerateClient @@ -114,6 +121,7 @@ should be used to generate input for the subsequent `graphqlGenerateClient` task | -------- | ---- | -------- | ----------- | | `endpoint` | String | yes | Target GraphQL server endpoint that will be used to execute introspection queries.
**Command line property is**: `endpoint`. | | `headers` | Map | | Optional HTTP headers to be specified on an introspection query. | +| `timeoutConfig` | TimeoutConfig | | Optional timeout configuration(in milliseconds) to execute introspection query before we cancel the request.
**Default value are:** connect timeout = 5_000, read timeout = 15_000.
| ## Documentation diff --git a/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLGradlePlugin.kt b/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLGradlePlugin.kt index 3773afcd0f..8c3c30e007 100644 --- a/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLGradlePlugin.kt +++ b/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLGradlePlugin.kt @@ -67,12 +67,14 @@ class GraphQLGradlePlugin : Plugin { val introspectSchemaTask = project.tasks.named(INTROSPECT_SCHEMA_TASK_NAME, GraphQLIntrospectSchemaTask::class.java).get() introspectSchemaTask.endpoint.convention(project.provider { extension.clientExtension.endpoint }) introspectSchemaTask.headers.convention(project.provider { extension.clientExtension.headers }) + introspectSchemaTask.timeoutConfig.convention(project.provider { extension.clientExtension.timeoutConfig }) generateClientTask.dependsOn(introspectSchemaTask.path) generateClientTask.schemaFile.convention(introspectSchemaTask.outputFile) } else if (extension.clientExtension.sdlEndpoint != null) { val downloadSDLTask = project.tasks.named(DOWNLOAD_SDL_TASK_NAME, GraphQLDownloadSDLTask::class.java).get() downloadSDLTask.endpoint.convention(project.provider { extension.clientExtension.sdlEndpoint }) downloadSDLTask.headers.convention(project.provider { extension.clientExtension.headers }) + downloadSDLTask.timeoutConfig.convention(project.provider { extension.clientExtension.timeoutConfig }) generateClientTask.dependsOn(downloadSDLTask.path) generateClientTask.schemaFile.convention(downloadSDLTask.outputFile) } else { diff --git a/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLPluginExtension.kt b/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLPluginExtension.kt index dd9aa26cea..429b020743 100644 --- a/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLPluginExtension.kt +++ b/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLPluginExtension.kt @@ -16,6 +16,7 @@ package com.expediagroup.graphql.plugin.gradle +import com.expediagroup.graphql.plugin.config.TimeoutConfig import com.expediagroup.graphql.plugin.generator.ScalarConverterMapping import java.io.File @@ -54,4 +55,11 @@ open class GraphQLPluginClientExtension { var converters: MutableMap = mutableMapOf() /** List of query files to be processed. */ var queryFiles: MutableList = mutableListOf() + + /** Connect and read timeout configuration for executing introspection query/download schema */ + internal val timeoutConfig: TimeoutConfig = TimeoutConfig() + + fun timeout(config: TimeoutConfig.() -> Unit = {}) { + timeoutConfig.apply(config) + } } diff --git a/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/tasks/GraphQLDownloadSDLTask.kt b/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/tasks/GraphQLDownloadSDLTask.kt index 9c5f482e58..6fb4c90cde 100755 --- a/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/tasks/GraphQLDownloadSDLTask.kt +++ b/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/tasks/GraphQLDownloadSDLTask.kt @@ -16,6 +16,7 @@ package com.expediagroup.graphql.plugin.gradle.tasks +import com.expediagroup.graphql.plugin.config.TimeoutConfig import com.expediagroup.graphql.plugin.downloadSchema import kotlinx.coroutines.runBlocking import org.gradle.api.DefaultTask @@ -49,6 +50,13 @@ open class GraphQLDownloadSDLTask : DefaultTask() { @Input val headers: MapProperty = project.objects.mapProperty(String::class.java, Any::class.java) + /** + * Timeout configuration that specifies maximum amount of time (in milliseconds) to connect and download schema from SDL endpoint before we cancel the request. + * Defaults to Ktor CIO engine defaults (5 seconds for connect timeout and 15 seconds for read timeout). + */ + @Input + val timeoutConfig: Property = project.objects.property(TimeoutConfig::class.java) + @OutputFile val outputFile: Provider = project.layout.buildDirectory.file("schema.graphql") @@ -57,6 +65,7 @@ open class GraphQLDownloadSDLTask : DefaultTask() { description = "Download schema in SDL format from target endpoint." headers.convention(emptyMap()) + timeoutConfig.convention(TimeoutConfig()) } /** @@ -67,7 +76,7 @@ open class GraphQLDownloadSDLTask : DefaultTask() { fun downloadSDLAction() { logger.debug("starting download SDL task against ${endpoint.get()}") runBlocking { - val schema = downloadSchema(endpoint = endpoint.get(), httpHeaders = headers.get()) + val schema = downloadSchema(endpoint = endpoint.get(), httpHeaders = headers.get(), timeoutConfig = timeoutConfig.get()) val outputFile = outputFile.get().asFile outputFile.writeText(schema) } diff --git a/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/tasks/GraphQLIntrospectSchemaTask.kt b/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/tasks/GraphQLIntrospectSchemaTask.kt index 090bbb0e0c..b205510156 100755 --- a/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/tasks/GraphQLIntrospectSchemaTask.kt +++ b/plugins/graphql-kotlin-gradle-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/gradle/tasks/GraphQLIntrospectSchemaTask.kt @@ -16,6 +16,7 @@ package com.expediagroup.graphql.plugin.gradle.tasks +import com.expediagroup.graphql.plugin.config.TimeoutConfig import com.expediagroup.graphql.plugin.introspectSchema import kotlinx.coroutines.runBlocking import org.gradle.api.DefaultTask @@ -49,6 +50,13 @@ open class GraphQLIntrospectSchemaTask : DefaultTask() { @Input val headers: MapProperty = project.objects.mapProperty(String::class.java, Any::class.java) + /** + * Timeout configuration that specifies maximum amount of time (in milliseconds) to connect and execute introspection query before we cancel the request. + * Defaults to Ktor CIO engine defaults (5 seconds for connect timeout and 15 seconds for read timeout). + */ + @Input + val timeoutConfig: Property = project.objects.property(TimeoutConfig::class.java) + @OutputFile val outputFile: Provider = project.layout.buildDirectory.file("schema.graphql") @@ -57,6 +65,7 @@ open class GraphQLIntrospectSchemaTask : DefaultTask() { description = "Run introspection query against target GraphQL endpoint and save schema locally." headers.convention(emptyMap()) + timeoutConfig.convention(TimeoutConfig()) } /** @@ -67,7 +76,7 @@ open class GraphQLIntrospectSchemaTask : DefaultTask() { fun introspectSchemaAction() { logger.debug("starting introspection task against ${endpoint.get()}") runBlocking { - val schema = introspectSchema(endpoint = endpoint.get(), httpHeaders = headers.get()) + val schema = introspectSchema(endpoint = endpoint.get(), httpHeaders = headers.get(), timeoutConfig = timeoutConfig.get()) outputFile.get().asFile.writeText(schema) } logger.debug("successfully created GraphQL schema from introspection result") diff --git a/plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLDownloadSDLTaskIT.kt b/plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLDownloadSDLTaskIT.kt index debd63e786..b53bad3132 100755 --- a/plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLDownloadSDLTaskIT.kt +++ b/plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLDownloadSDLTaskIT.kt @@ -77,4 +77,29 @@ class GraphQLDownloadSDLTaskIT : GraphQLGradlePluginAbstractIT() { assertEquals(TaskOutcome.SUCCESS, result.task(":$DOWNLOAD_SDL_TASK_NAME")?.outcome) assertTrue(File(tempDir.toFile(), "build/schema.graphql").exists()) } + + @Test + fun `apply the gradle plugin and execute downloadSDL with timeout`(@TempDir tempDir: Path) { + val testProjectDirectory = tempDir.toFile() + WireMock.reset() + WireMock.stubFor(stubSdlEndpoint(delay = 10_000)) + + val buildFileContents = + """ + val graphqlDownloadSDL by tasks.getting(GraphQLDownloadSDLTask::class) { + endpoint.set("${wireMockServer.baseUrl()}/sdl") + timeoutConfig.set(TimeoutConfig(connect = 100, read = 100)) + } + """.trimIndent() + testProjectDirectory.generateBuildFile(buildFileContents) + + val result = GradleRunner.create() + .withProjectDir(testProjectDirectory) + .withPluginClasspath() + .withArguments(DOWNLOAD_SDL_TASK_NAME) + .buildAndFail() + + assertEquals(TaskOutcome.FAILED, result.task(":$DOWNLOAD_SDL_TASK_NAME")?.outcome) + assertTrue(result.output.contains("Timed out waiting for 100 ms", ignoreCase = true)) + } } diff --git a/plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLGradlePluginAbstractIT.kt b/plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLGradlePluginAbstractIT.kt index bc223ad534..44eb9b7114 100755 --- a/plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLGradlePluginAbstractIT.kt +++ b/plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLGradlePluginAbstractIT.kt @@ -51,22 +51,23 @@ abstract class GraphQLGradlePluginAbstractIT { WireMock.stubFor(stubGraphQLResponse()) } - fun stubSdlEndpoint(): MappingBuilder = WireMock.get("/sdl") - .withResponse(content = testSchema, contentType = "text/plain") + fun stubSdlEndpoint(delay: Int? = null): MappingBuilder = WireMock.get("/sdl") + .withResponse(content = testSchema, contentType = "text/plain", delay = delay) - fun stubIntrospectionResult(): MappingBuilder = WireMock.post("/graphql") + fun stubIntrospectionResult(delay: Int? = null): MappingBuilder = WireMock.post("/graphql") .withRequestBody(ContainsPattern("IntrospectionQuery")) - .withResponse(content = introspectionResult) + .withResponse(content = introspectionResult, delay = delay) - fun stubGraphQLResponse(): MappingBuilder = WireMock.post("/graphql") + fun stubGraphQLResponse(delay: Int? = null): MappingBuilder = WireMock.post("/graphql") .withRequestBody(ContainsPattern("JUnitQuery")) - .withResponse(content = testResponse) + .withResponse(content = testResponse, delay = delay) - private fun MappingBuilder.withResponse(content: String, contentType: String = "application/json") = this.willReturn( + private fun MappingBuilder.withResponse(content: String, contentType: String = "application/json", delay: Int? = null) = this.willReturn( WireMock.aResponse() .withStatus(200) .withHeader("Content-Type", contentType) .withBody(content) + .withFixedDelay(delay ?: 0) ) fun loadResource(resourceName: String) = ClassLoader.getSystemClassLoader().getResourceAsStream(resourceName)?.use { @@ -82,6 +83,7 @@ abstract class GraphQLGradlePluginAbstractIT { val buildFileContents = """ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + import com.expediagroup.graphql.plugin.config.TimeoutConfig import com.expediagroup.graphql.plugin.generator.ScalarConverterMapping import com.expediagroup.graphql.plugin.gradle.graphql import com.expediagroup.graphql.plugin.gradle.tasks.GraphQLDownloadSDLTask diff --git a/plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLIntrospectSchemaTaskIT.kt b/plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLIntrospectSchemaTaskIT.kt index 4529809a3c..2e55833e6f 100755 --- a/plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLIntrospectSchemaTaskIT.kt +++ b/plugins/graphql-kotlin-gradle-plugin/src/test/kotlin/com/expediagroup/graphql/plugin/gradle/GraphQLIntrospectSchemaTaskIT.kt @@ -76,4 +76,29 @@ class GraphQLIntrospectSchemaTaskIT : GraphQLGradlePluginAbstractIT() { assertEquals(TaskOutcome.SUCCESS, result.task(":$INTROSPECT_SCHEMA_TASK_NAME")?.outcome) assertTrue(File(testProjectDirectory, "build/schema.graphql").exists()) } + + @Test + fun `apply the gradle plugin and execute introspectSchema with timeout`(@TempDir tempDir: Path) { + val testProjectDirectory = tempDir.toFile() + WireMock.reset() + WireMock.stubFor(stubIntrospectionResult(delay = 10_000)) + + val buildFileContents = + """ + val graphqlIntrospectSchema by tasks.getting(GraphQLIntrospectSchemaTask::class) { + endpoint.set("${wireMockServer.baseUrl()}/graphql") + timeoutConfig.set(TimeoutConfig(connect = 100, read = 100)) + } + """.trimIndent() + testProjectDirectory.generateBuildFile(buildFileContents) + + val result = GradleRunner.create() + .withProjectDir(testProjectDirectory) + .withPluginClasspath() + .withArguments(INTROSPECT_SCHEMA_TASK_NAME) + .buildAndFail() + + assertEquals(TaskOutcome.FAILED, result.task(":$INTROSPECT_SCHEMA_TASK_NAME")?.outcome) + assertTrue(result.output.contains("Timed out waiting for 100 ms", ignoreCase = true)) + } } diff --git a/plugins/graphql-kotlin-maven-plugin/README.md b/plugins/graphql-kotlin-maven-plugin/README.md index f07fa5654a..f043218d61 100755 --- a/plugins/graphql-kotlin-maven-plugin/README.md +++ b/plugins/graphql-kotlin-maven-plugin/README.md @@ -24,10 +24,8 @@ Plugin should be configured as part of your `pom.xml` build file. http://localhost:8080/graphql com.example.generated ${project.build.directory}/schema.graphql + true - - My-Custom-Header - @@ -38,6 +36,14 @@ Plugin should be configured as part of your `pom.xml` build file. com.example.UUIDScalarConverter + + My-Custom-Header + + + + 1000 + 30000 + ${project.basedir}/src/main/resources/queries/MyQuery.graphql @@ -65,6 +71,19 @@ by itself and instead should be used to generate input for the subsequent `gener | -------- | ---- | -------- | ----------- | | `endpoint` | String | yes | Target GraphQL server SDL endpoint that will be used to download schema.
**User property is**: `graphql.endpoint`. | | `headers` | Map | | Optional HTTP headers to be specified on a SDL request. +| `timeoutConfiguration` | TimeoutConfiguration | | Optional timeout configuration (in milliseconds) to download schema from SDL endpoint before we cancel the request.
**Default values are:** connect timeout = 5000, read timeout = 15000.
| + +**Parameter Details** + + * *timeoutConfiguration* - Timeout configuration that allows you to specify connect and read timeout values in milliseconds. + + ```xml + + + 1000 + 30000 + + ``` ### generate-client @@ -160,6 +179,19 @@ should be used to generate input for the subsequent `generate-client` goal. | -------- | ---- | -------- | ----------- | | `endpoint` | String | yes | Target GraphQL server endpoint that will be used to execute introspection queries.
**User property is**: `graphql.endpoint`. | | `headers` | Map | | Optional HTTP headers to be specified on an introspection query. | +| `timeoutConfiguration` | TimeoutConfiguration | | Optional timeout configuration(in milliseconds) to execute introspection query before we cancel the request.
**Default values are:** connect timeout = 5000, read timeout = 15000.
| + +**Parameter Details** + + * *timeoutConfiguration* - Timeout configuration that allows you to specify connect and read timeout values in milliseconds. + + ```xml + + + 1000 + 30000 + + ``` ## Documentation diff --git a/plugins/graphql-kotlin-maven-plugin/pom.xml b/plugins/graphql-kotlin-maven-plugin/pom.xml index 983a7e9b9c..15e6106a2d 100755 --- a/plugins/graphql-kotlin-maven-plugin/pom.xml +++ b/plugins/graphql-kotlin-maven-plugin/pom.xml @@ -129,6 +129,7 @@ src/integration src/integration/settings.xml + true diff --git a/plugins/graphql-kotlin-maven-plugin/src/integration/download-sdl-timeout/invoker.properties b/plugins/graphql-kotlin-maven-plugin/src/integration/download-sdl-timeout/invoker.properties new file mode 100644 index 0000000000..fdd3d204d5 --- /dev/null +++ b/plugins/graphql-kotlin-maven-plugin/src/integration/download-sdl-timeout/invoker.properties @@ -0,0 +1 @@ +invoker.buildResult=failure diff --git a/plugins/graphql-kotlin-maven-plugin/src/integration/download-sdl-timeout/pom.xml b/plugins/graphql-kotlin-maven-plugin/src/integration/download-sdl-timeout/pom.xml new file mode 100755 index 0000000000..5a7ba37d36 --- /dev/null +++ b/plugins/graphql-kotlin-maven-plugin/src/integration/download-sdl-timeout/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + + com.expediagroup + graphql-kotlin-download-sdl-mojo-timeout-test + 1.0-SNAPSHOT + jar + + + + org.jetbrains.kotlin + kotlin-stdlib + @kotlin.version@ + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + @kotlin-coroutines.version@ + + + io.ktor + ktor-client-cio + @ktor.version@ + + + io.ktor + ktor-client-json + @ktor.version@ + + + io.ktor + ktor-client-jackson + @ktor.version@ + + + org.junit.jupiter + junit-jupiter + @junit.version@ + test + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + @kotlin.version@ + + @kotlin.jvmTarget@ + + + + compile + + compile + + + + test-compile + + test-compile + + + + + + com.expediagroup + graphql-kotlin-maven-plugin + @graphql-kotlin.version@ + + + + download-sdl + + + @graphql.endpoint@/sdlTimeout + + 100 + 100 + + + + + + + com.expediagroup + graphql-kotlin-plugin-core + @graphql-kotlin.version@ + + + com.graphql-java + graphql-java + @graphql-java.version@ + + + com.squareup + kotlinpoet + @kotlinpoet.version@ + + + io.ktor + ktor-client-cio + @ktor.version@ + + + io.ktor + ktor-client-json + @ktor.version@ + + + io.ktor + ktor-client-jackson + @ktor.version@ + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + @kotlin-coroutines.version@ + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M4 + + + ${project.build.directory} + @graphql.endpoint@/graphql + + + + + + diff --git a/plugins/graphql-kotlin-maven-plugin/src/integration/generate-test-client/pom.xml b/plugins/graphql-kotlin-maven-plugin/src/integration/generate-test-client/pom.xml index a18be0e333..cef48687c7 100755 --- a/plugins/graphql-kotlin-maven-plugin/src/integration/generate-test-client/pom.xml +++ b/plugins/graphql-kotlin-maven-plugin/src/integration/generate-test-client/pom.xml @@ -6,7 +6,7 @@ com.expediagroup - graphql-kotlin-generate-client-mojo-test + graphql-kotlin-generate-test-client-mojo-test 1.0-SNAPSHOT jar diff --git a/plugins/graphql-kotlin-maven-plugin/src/integration/introspect-timeout/invoker.properties b/plugins/graphql-kotlin-maven-plugin/src/integration/introspect-timeout/invoker.properties new file mode 100644 index 0000000000..fdd3d204d5 --- /dev/null +++ b/plugins/graphql-kotlin-maven-plugin/src/integration/introspect-timeout/invoker.properties @@ -0,0 +1 @@ +invoker.buildResult=failure diff --git a/plugins/graphql-kotlin-maven-plugin/src/integration/introspect-timeout/pom.xml b/plugins/graphql-kotlin-maven-plugin/src/integration/introspect-timeout/pom.xml new file mode 100755 index 0000000000..f59d63af91 --- /dev/null +++ b/plugins/graphql-kotlin-maven-plugin/src/integration/introspect-timeout/pom.xml @@ -0,0 +1,146 @@ + + + 4.0.0 + + + com.expediagroup + graphql-kotlin-introspect-schema-mojo-timeout-test + 1.0-SNAPSHOT + jar + + + + org.jetbrains.kotlin + kotlin-stdlib + @kotlin.version@ + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + @kotlin-coroutines.version@ + + + io.ktor + ktor-client-cio + @ktor.version@ + + + io.ktor + ktor-client-json + @ktor.version@ + + + io.ktor + ktor-client-jackson + @ktor.version@ + + + org.junit.jupiter + junit-jupiter + @junit.version@ + test + + + + + src/main/kotlin + src/test/kotlin + + + org.jetbrains.kotlin + kotlin-maven-plugin + @kotlin.version@ + + + compile + + compile + + + + + test-compile + + test-compile + + + + + + com.expediagroup + graphql-kotlin-maven-plugin + @graphql-kotlin.version@ + + @kotlin.jvmTarget@ + + + + + introspect-schema + + + @graphql.endpoint@/graphqlTimeout + + 100 + 100 + + + true + + + + + + + com.expediagroup + graphql-kotlin-plugin-core + @graphql-kotlin.version@ + + + com.graphql-java + graphql-java + @graphql-java.version@ + + + com.squareup + kotlinpoet + @kotlinpoet.version@ + + + io.ktor + ktor-client-cio + @ktor.version@ + + + io.ktor + ktor-client-json + @ktor.version@ + + + io.ktor + ktor-client-jackson + @ktor.version@ + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + @kotlin-coroutines.version@ + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M4 + + + ${project.build.directory} + @graphql.endpoint@/graphql + + + + + + diff --git a/plugins/graphql-kotlin-maven-plugin/src/integration/wiremock/mappings/introspectionTimeout.json b/plugins/graphql-kotlin-maven-plugin/src/integration/wiremock/mappings/introspectionTimeout.json new file mode 100755 index 0000000000..4941b93ff1 --- /dev/null +++ b/plugins/graphql-kotlin-maven-plugin/src/integration/wiremock/mappings/introspectionTimeout.json @@ -0,0 +1,22 @@ +{ + "request": { + "method": "POST", + "url": "/graphqlTimeout", + "bodyPatterns": [{ + "contains": "IntrospectionQuery" + }], + "headers": { + "X-Delay-Header": { + "equalTo": "true" + } + } + }, + "response": { + "status": 200, + "bodyFileName": "introspectionResult.json", + "headers": { + "Content-Type": "application/json" + }, + "fixedDelayMilliseconds": 10000 + } +} diff --git a/plugins/graphql-kotlin-maven-plugin/src/integration/wiremock/mappings/sdlTimeout.json b/plugins/graphql-kotlin-maven-plugin/src/integration/wiremock/mappings/sdlTimeout.json new file mode 100755 index 0000000000..d642ae869d --- /dev/null +++ b/plugins/graphql-kotlin-maven-plugin/src/integration/wiremock/mappings/sdlTimeout.json @@ -0,0 +1,14 @@ +{ + "request": { + "method": "GET", + "url": "/sdlTimeout" + }, + "response": { + "status": 200, + "bodyFileName": "testSchema.graphql", + "headers": { + "Content-Type": "text/plain" + }, + "fixedDelayMilliseconds": 10000 + } +} diff --git a/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/DownloadSDLMojo.kt b/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/DownloadSDLMojo.kt index 3566ba1a8d..5baa617be9 100644 --- a/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/DownloadSDLMojo.kt +++ b/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/DownloadSDLMojo.kt @@ -16,46 +16,18 @@ package com.expediagroup.graphql.plugin.maven +import com.expediagroup.graphql.plugin.config.TimeoutConfig import com.expediagroup.graphql.plugin.downloadSchema -import kotlinx.coroutines.runBlocking -import org.apache.maven.plugin.AbstractMojo import org.apache.maven.plugins.annotations.LifecyclePhase import org.apache.maven.plugins.annotations.Mojo -import org.apache.maven.plugins.annotations.Parameter -import java.io.File /** * Download GraphQL schema from a specified SDL endpoint. */ @Mojo(name = "download-sdl", defaultPhase = LifecyclePhase.GENERATE_SOURCES) -class DownloadSDLMojo : AbstractMojo() { - - /** - * Target GraphQL server SDL endpoint. - */ - @Parameter(defaultValue = "\${graphql.endpoint}", name = "endpoint", required = true) - private lateinit var endpoint: String - - /** - * Optional HTTP headers to be specified on a SDL request. - */ - @Parameter(name = "headers") - private var headers: Map = mutableMapOf() - - @Parameter(defaultValue = "\${project.build.directory}", readonly = true) - private lateinit var outputDirectory: File +class DownloadSDLMojo : RetrieveSchemaAbstractMojo() { @Suppress("EXPERIMENTAL_API_USAGE") - override fun execute() { - log.debug("executing downloadSDL MOJO against $endpoint") - if (!outputDirectory.isDirectory) { - outputDirectory.mkdirs() - } - val schemaFile = File("${outputDirectory.absolutePath}/schema.graphql") - runBlocking { - val schema = downloadSchema(endpoint = endpoint, httpHeaders = headers) - schemaFile.writeText(schema) - } - log.debug("successfully downloaded SDL") - } + override suspend fun retrieveGraphQLSchema(endpoint: String, httpHeaders: Map, timeoutConfig: TimeoutConfig): String = + downloadSchema(endpoint = endpoint, httpHeaders = httpHeaders, timeoutConfig = timeoutConfig) } diff --git a/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/IntrospectSchemaMojo.kt b/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/IntrospectSchemaMojo.kt index 99a324f686..9a7868532c 100644 --- a/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/IntrospectSchemaMojo.kt +++ b/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/IntrospectSchemaMojo.kt @@ -16,47 +16,18 @@ package com.expediagroup.graphql.plugin.maven +import com.expediagroup.graphql.plugin.config.TimeoutConfig import com.expediagroup.graphql.plugin.introspectSchema -import kotlinx.coroutines.runBlocking -import org.apache.maven.plugin.AbstractMojo import org.apache.maven.plugins.annotations.LifecyclePhase import org.apache.maven.plugins.annotations.Mojo -import org.apache.maven.plugins.annotations.Parameter -import java.io.File /** * Run introspection query against specified endpoint and save resulting GraphQL schema locally. */ @Mojo(name = "introspect-schema", defaultPhase = LifecyclePhase.GENERATE_SOURCES) -class IntrospectSchemaMojo : AbstractMojo() { - - /** - * Target GraphQL server endpoint. - */ - @Parameter(defaultValue = "\${graphql.endpoint}", name = "endpoint", required = true) - private lateinit var endpoint: String - - /** - * Optional HTTP headers to be specified on an introspection query. - */ - @Parameter(name = "headers") - private var headers: Map = mutableMapOf() - - @Parameter(defaultValue = "\${project.build.directory}", readonly = true) - private lateinit var outputDirectory: File +class IntrospectSchemaMojo : RetrieveSchemaAbstractMojo() { @Suppress("EXPERIMENTAL_API_USAGE") - override fun execute() { - log.debug("executing introspectSchema MOJO against $endpoint") - if (!outputDirectory.isDirectory) { - outputDirectory.mkdirs() - } - - val schemaFile = File("${outputDirectory.absolutePath}/schema.graphql") - runBlocking { - val schema = introspectSchema(endpoint = endpoint, httpHeaders = headers) - schemaFile.writeText(schema) - } - log.debug("successfully generated schema from introspection results") - } + override suspend fun retrieveGraphQLSchema(endpoint: String, httpHeaders: Map, timeoutConfig: TimeoutConfig): String = + introspectSchema(endpoint = endpoint, httpHeaders = httpHeaders, timeoutConfig = timeoutConfig) } diff --git a/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/RetrieveSchemaAbstractMojo.kt b/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/RetrieveSchemaAbstractMojo.kt new file mode 100644 index 0000000000..f295909c52 --- /dev/null +++ b/plugins/graphql-kotlin-maven-plugin/src/main/kotlin/com/expediagroup/graphql/plugin/maven/RetrieveSchemaAbstractMojo.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2020 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.plugin.maven + +import com.expediagroup.graphql.plugin.config.TimeoutConfig +import kotlinx.coroutines.runBlocking +import org.apache.maven.plugin.AbstractMojo +import org.apache.maven.plugins.annotations.Parameter +import java.io.File + +/** + * Retrieve GraphQL schema by running introspection query or by downloading it directly from a specified SDL endpoint. + */ +abstract class RetrieveSchemaAbstractMojo : AbstractMojo() { + + /** + * Target endpoint. + */ + @Parameter(defaultValue = "\${graphql.endpoint}", name = "endpoint", required = true) + private lateinit var endpoint: String + + /** + * Optional HTTP headers to be specified on a request. + */ + @Parameter(name = "headers") + private var headers: Map = mutableMapOf() + + /** + * Timeout configuration that specifies maximum amount of time (in milliseconds) to connect and download schema before we cancel the request. + * Defaults to Ktor CIO engine defaults (5 seconds for connect timeout and 15 seconds for read timeout). + */ + @Parameter(name = "timeoutConfiguration") + private var timeoutConfiguration: TimeoutConfiguration = TimeoutConfiguration() + + @Parameter(defaultValue = "\${project.build.directory}", readonly = true) + private lateinit var outputDirectory: File + + override fun execute() { + log.debug("downloading GraphQL schema from $endpoint") + if (!outputDirectory.isDirectory) { + outputDirectory.mkdirs() + } + val schemaFile = File("${outputDirectory.absolutePath}/schema.graphql") + runBlocking { + val schema = retrieveGraphQLSchema(endpoint, headers, TimeoutConfig(connect = timeoutConfiguration.connect, read = timeoutConfiguration.read)) + schemaFile.writeText(schema) + } + log.debug("successfully downloaded schema") + } + + abstract suspend fun retrieveGraphQLSchema(endpoint: String, httpHeaders: Map, timeoutConfig: TimeoutConfig): String +} + +/** + * Maven Plugin Property equivalent of [TimeoutConfig]. + * + * Unfortunately we cannot use [TimeoutConfig] directly as per rules of mapping complex objects to Mojo parameters, target object has to be declared in + * the same package as Mojo itself (otherwise we need to explicitly specify fully qualified implementation name in configuration XML block). + * + * @see [Guide to Configuring Plug-ins](https://maven.apache.org/guides/mini/guide-configuring-plugins.html#Mapping_Complex_Objects) + */ +class TimeoutConfiguration { + /** Timeout in milliseconds to establish new connection. */ + @Parameter + var connect: Long = 5_000 + + /** Read timeout in milliseconds */ + @Parameter + var read: Long = 15_000 +} diff --git a/plugins/graphql-kotlin-plugin-core/src/main/kotlin/com/expediagroup/graphql/plugin/config/TimeoutConfig.kt b/plugins/graphql-kotlin-plugin-core/src/main/kotlin/com/expediagroup/graphql/plugin/config/TimeoutConfig.kt new file mode 100644 index 0000000000..b84fcee285 --- /dev/null +++ b/plugins/graphql-kotlin-plugin-core/src/main/kotlin/com/expediagroup/graphql/plugin/config/TimeoutConfig.kt @@ -0,0 +1,13 @@ +package com.expediagroup.graphql.plugin.config + +import java.io.Serializable + +/** + * Timeout configuration for executing introspection query and downloading schema SDL. + */ +data class TimeoutConfig( + /** Timeout in milliseconds to establish new connection. */ + var connect: Long = 5_000, + /** Read timeout in milliseconds */ + var read: Long = 15_000 +) : Serializable diff --git a/plugins/graphql-kotlin-plugin-core/src/main/kotlin/com/expediagroup/graphql/plugin/downloadSchema.kt b/plugins/graphql-kotlin-plugin-core/src/main/kotlin/com/expediagroup/graphql/plugin/downloadSchema.kt index 295eb2e984..64809a7f8a 100644 --- a/plugins/graphql-kotlin-plugin-core/src/main/kotlin/com/expediagroup/graphql/plugin/downloadSchema.kt +++ b/plugins/graphql-kotlin-plugin-core/src/main/kotlin/com/expediagroup/graphql/plugin/downloadSchema.kt @@ -16,18 +16,29 @@ package com.expediagroup.graphql.plugin +import com.expediagroup.graphql.plugin.config.TimeoutConfig import graphql.schema.idl.SchemaParser import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO +import io.ktor.client.engine.cio.endpoint +import io.ktor.client.features.ClientRequestException import io.ktor.client.request.get import io.ktor.client.request.header import io.ktor.util.KtorExperimentalAPI +import kotlinx.coroutines.TimeoutCancellationException /** * Downloads GraphQL SDL from the specified endpoint and verifies whether the result is a valid GraphQL schema. */ @KtorExperimentalAPI -suspend fun downloadSchema(endpoint: String, httpHeaders: Map = emptyMap()): String = HttpClient(CIO).use { client -> +suspend fun downloadSchema(endpoint: String, httpHeaders: Map = emptyMap(), timeoutConfig: TimeoutConfig = TimeoutConfig()): String = HttpClient(engineFactory = CIO) { + engine { + requestTimeout = timeoutConfig.read + endpoint { + connectTimeout = timeoutConfig.connect + } + } +}.use { client -> val sdl = try { client.get(urlString = endpoint) { httpHeaders.forEach { (name, value) -> @@ -35,7 +46,10 @@ suspend fun downloadSchema(endpoint: String, httpHeaders: Map = emp } } } catch (e: Throwable) { - throw RuntimeException("Unable to download SDL from specified endpoint=$endpoint", e) + when (e) { + is ClientRequestException, is TimeoutCancellationException -> throw e + else -> throw RuntimeException("Unable to download SDL from specified endpoint=$endpoint", e) + } } SchemaParser().parse(sdl) sdl diff --git a/plugins/graphql-kotlin-plugin-core/src/main/kotlin/com/expediagroup/graphql/plugin/introspectSchema.kt b/plugins/graphql-kotlin-plugin-core/src/main/kotlin/com/expediagroup/graphql/plugin/introspectSchema.kt index 336db462a7..7e11a54e8e 100644 --- a/plugins/graphql-kotlin-plugin-core/src/main/kotlin/com/expediagroup/graphql/plugin/introspectSchema.kt +++ b/plugins/graphql-kotlin-plugin-core/src/main/kotlin/com/expediagroup/graphql/plugin/introspectSchema.kt @@ -16,11 +16,14 @@ package com.expediagroup.graphql.plugin +import com.expediagroup.graphql.plugin.config.TimeoutConfig import graphql.introspection.IntrospectionQuery import graphql.introspection.IntrospectionResultToSchema import graphql.schema.idl.SchemaPrinter import io.ktor.client.HttpClient import io.ktor.client.engine.cio.CIO +import io.ktor.client.engine.cio.endpoint +import io.ktor.client.features.ClientRequestException import io.ktor.client.features.json.JsonFeature import io.ktor.client.request.accept import io.ktor.client.request.header @@ -29,12 +32,19 @@ import io.ktor.client.request.url import io.ktor.http.ContentType import io.ktor.http.contentType import io.ktor.util.KtorExperimentalAPI +import kotlinx.coroutines.TimeoutCancellationException /** * Runs introspection query against specified GraphQL endpoint and returns underlying schema. */ @KtorExperimentalAPI -suspend fun introspectSchema(endpoint: String, httpHeaders: Map = emptyMap()): String = HttpClient(engineFactory = CIO) { +suspend fun introspectSchema(endpoint: String, httpHeaders: Map = emptyMap(), timeoutConfig: TimeoutConfig = TimeoutConfig()): String = HttpClient(engineFactory = CIO) { + engine { + requestTimeout = timeoutConfig.read + endpoint { + connectTimeout = timeoutConfig.connect + } + } install(feature = JsonFeature) }.use { client -> val introspectionResult = try { @@ -50,8 +60,11 @@ suspend fun introspectSchema(endpoint: String, httpHeaders: Map = e "operationName" to "IntrospectionQuery" ) } - } catch (e: Error) { - throw RuntimeException("Unable to run introspection query against the specified endpoint=$endpoint") + } catch (e: Throwable) { + when (e) { + is ClientRequestException, is TimeoutCancellationException -> throw e + else -> throw RuntimeException("Unable to run introspection query against the specified endpoint=$endpoint", e) + } } @Suppress("UNCHECKED_CAST") diff --git a/plugins/graphql-kotlin-plugin-core/src/test/kotlin/com/expediagroup/graphql/plugin/DownloadSchemaTest.kt b/plugins/graphql-kotlin-plugin-core/src/test/kotlin/com/expediagroup/graphql/plugin/DownloadSchemaTest.kt index 3e17a2274c..1f149248f3 100644 --- a/plugins/graphql-kotlin-plugin-core/src/test/kotlin/com/expediagroup/graphql/plugin/DownloadSchemaTest.kt +++ b/plugins/graphql-kotlin-plugin-core/src/test/kotlin/com/expediagroup/graphql/plugin/DownloadSchemaTest.kt @@ -16,20 +16,26 @@ package com.expediagroup.graphql.plugin +import com.expediagroup.graphql.plugin.config.TimeoutConfig import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.client.WireMock import com.github.tomakehurst.wiremock.client.WireMock.aResponse import com.github.tomakehurst.wiremock.client.WireMock.get import com.github.tomakehurst.wiremock.client.WireMock.stubFor import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import graphql.schema.idl.errors.SchemaProblem +import io.ktor.client.features.ClientRequestException import io.ktor.util.KtorExperimentalAPI +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import java.nio.channels.UnresolvedAddressException import kotlin.test.assertEquals +import kotlin.test.assertTrue class DownloadSchemaTest { @@ -92,44 +98,63 @@ class DownloadSchemaTest { @Test @KtorExperimentalAPI fun `verify downloadSchema will throw exception if URL is not valid`() { - assertThrows { + val exception = assertThrows { runBlocking { - downloadSchema("http://non-existent-graphql-url.com") + downloadSchema("https://non-existent-graphql-url.com/should_404") } } + assertTrue(exception.cause is UnresolvedAddressException) } @Test @KtorExperimentalAPI fun `verify downloadSchema will throw exception if downloaded SDL is not valid schema`() { stubFor( - get("whatever").willReturn( + get("/whatever").willReturn( aResponse() .withStatus(200) .withHeader("Content-Type", "text/plain") .withBody("some random body") ) ) - assertThrows { + val exception = assertThrows { runBlocking { downloadSchema(endpoint = "${wireMockServer.baseUrl()}/whatever") } } + assertTrue(exception is SchemaProblem) } @Test @KtorExperimentalAPI fun `verify downloadSchema will throw exception if unable to download schema`() { stubFor( - get("sdl").willReturn(aResponse().withStatus(404)) + get("/sdl").willReturn(aResponse().withStatus(404)) ) - assertThrows { + assertThrows { runBlocking { downloadSchema("${wireMockServer.baseUrl()}/sdl") } } } + @Test + @KtorExperimentalAPI + fun `verify downloadSchema will respect timeout setting`() { + stubFor( + get("/sdl").willReturn( + aResponse() + .withStatus(200) + .withFixedDelay(1_000) + ) + ) + assertThrows { + runBlocking { + downloadSchema(endpoint = "${wireMockServer.baseUrl()}/sdl", timeoutConfig = TimeoutConfig(connect = 100, read = 100)) + } + } + } + companion object { private val wireMockServer: WireMockServer = WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()) diff --git a/plugins/graphql-kotlin-plugin-core/src/test/kotlin/com/expediagroup/graphql/plugin/IntrospectSchemaTest.kt b/plugins/graphql-kotlin-plugin-core/src/test/kotlin/com/expediagroup/graphql/plugin/IntrospectSchemaTest.kt index 9556320d38..cfc15f83f8 100755 --- a/plugins/graphql-kotlin-plugin-core/src/test/kotlin/com/expediagroup/graphql/plugin/IntrospectSchemaTest.kt +++ b/plugins/graphql-kotlin-plugin-core/src/test/kotlin/com/expediagroup/graphql/plugin/IntrospectSchemaTest.kt @@ -16,10 +16,13 @@ package com.expediagroup.graphql.plugin +import com.expediagroup.graphql.plugin.config.TimeoutConfig import com.github.tomakehurst.wiremock.WireMockServer import com.github.tomakehurst.wiremock.client.WireMock import com.github.tomakehurst.wiremock.core.WireMockConfiguration +import io.ktor.client.features.ClientRequestException import io.ktor.util.KtorExperimentalAPI +import kotlinx.coroutines.TimeoutCancellationException import kotlinx.coroutines.runBlocking import org.junit.jupiter.api.AfterAll import org.junit.jupiter.api.BeforeAll @@ -106,16 +109,34 @@ class IntrospectSchemaTest { @KtorExperimentalAPI fun `verify introspectSchema will throw exception if unable to run query`() { WireMock.stubFor( - WireMock.post("graphql") + WireMock.post("/graphql") .willReturn(WireMock.aResponse().withStatus(404)) ) - assertThrows { + assertThrows { runBlocking { introspectSchema("${wireMockServer.baseUrl()}/graphql") } } } + @Test + @KtorExperimentalAPI + fun `verify introspectSchema will respect timeout setting`() { + WireMock.stubFor( + WireMock.post("/graphql") + .willReturn( + WireMock.aResponse() + .withStatus(200) + .withFixedDelay(1_000) + ) + ) + assertThrows { + runBlocking { + introspectSchema(endpoint = "${wireMockServer.baseUrl()}/graphql", timeoutConfig = TimeoutConfig(connect = 100, read = 100)) + } + } + } + companion object { private val wireMockServer: WireMockServer = WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort())