Skip to content

Commit

Permalink
GraalVM example integration (#1742)
Browse files Browse the repository at this point in the history
Adds new `graalVM` composite build that ensures servers (ktor for now) can be build with GraphQL Kotlin plugin and produce valid GraalVM native image.
  • Loading branch information
dariuszkuc authored Apr 11, 2023
1 parent f4ba9b4 commit 2902678
Show file tree
Hide file tree
Showing 32 changed files with 1,053 additions and 1 deletion.
78 changes: 78 additions & 0 deletions .github/workflows/graalvm-integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
name: GraaLVM Integration Tests

on:
workflow_call:
pull_request:
branches:
- master
paths:
- 'generator/**'
- 'servers/**'
- 'plugins/**'
- 'integration/graalvm/**'

jobs:
graalvm-integration:
timeout-minutes: 20
runs-on: ubuntu-latest
defaults:
run:
working-directory: integration/graalvm
strategy:
matrix:
server: ['ktor-graalvm-server']

steps:
- name: Checkout Repository
uses: actions/checkout@v3

- name: Validate Gradle wrapper
uses: gradle/wrapper-validation-action@v1

- name: Setup GraalVM
uses: graalvm/setup-graalvm@v1
with:
version: '22.3.1'
java-version: '17'
components: 'native-image'
native-image-job-reports: 'true'

- name: Set up Gradle cache
uses: gradle/gradle-build-action@v2

- name: Build server
run: ./gradlew :${server}:build :${server}:nativeCompile
env:
server: ${{ matrix.server }}

- name: Build and start the native image
id: start_server
run: |
set -x
echo "starting server"
./${server}/build/native/nativeCompile/${server} &
echo "SERVER_PID=$(echo $!)" >> $GITHUB_OUTPUT
env:
server: ${{ matrix.server }}

- name: Integration Test
run: |
echo "sending a test query"
curl --request POST \
--verbose \
--header 'content-type: application/json' \
--url http://localhost:8080/graphql \
--data '{"query":"query($inputArg: InputOnlyInput){ inputTypeQuery(arg: $inputArg) }","variables":{"inputArg": { "id": 123 }}}' \
> response.json
echo "received GraphQL response"
cat response.json
echo "verifying response"
jq -e '.data.inputTypeQuery == "InputOnly(id=123)"' response.json
- name: Stop server
if: ${{ always() }}
run: |
echo "shutting down server"
kill -9 ${{ steps.start_server.outputs.SERVER_PID }}
20 changes: 20 additions & 0 deletions integration/graalvm/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import java.util.Properties

allprojects {
repositories {
mavenCentral()
mavenLocal {
content {
includeGroup("com.expediagroup")
}
}
}

val properties = Properties()
properties.load(File(rootDir.parentFile.parent, "gradle.properties").inputStream())
for ((key, value) in properties) {
if (!project.ext.has(key.toString())) {
project.ext[key.toString()] = value
}
}
}
6 changes: 6 additions & 0 deletions integration/graalvm/gradle.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
group = com.expediagroup.graalvm

org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=1g
org.gradle.caching=true
org.gradle.parallel=true

1 change: 1 addition & 0 deletions integration/graalvm/gradle/wrapper
1 change: 1 addition & 0 deletions integration/graalvm/gradlew
53 changes: 53 additions & 0 deletions integration/graalvm/ktor-graalvm-server/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import com.expediagroup.graphql.plugin.gradle.graphql
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

@Suppress("DSL_SCOPE_VIOLATION") // TODO: remove once KTIJ-19369 / Gradle#22797 is fixed
plugins {
alias(libs.plugins.kotlin.jvm)
application
alias(libs.plugins.graalvm.native)
id("com.expediagroup.graphql")
}

dependencies {
implementation("com.expediagroup", "graphql-kotlin-ktor-server")
implementation("com.expediagroup", "graphql-kotlin-hooks-provider")
implementation(libs.logback)
implementation(libs.ktor.server.cio)
testImplementation(libs.junit.api)
testImplementation(libs.kotlin.test)
testImplementation(libs.ktor.client.content)
testImplementation(libs.ktor.server.test.host)
}

tasks.test {
useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "17"
}

application {
mainClass.set("com.expediagroup.graalvm.ktor.ApplicationKt")
}

graalvmNative {
toolchainDetection.set(false)
binaries {
named("main") {
verbose.set(true)
buildArgs.add("--initialize-at-build-time=io.ktor,kotlin,ch.qos.logback,org.slf4j")
buildArgs.add("-H:+ReportExceptionStackTraces")
}
metadataRepository {
enabled.set(true)
}
}
}

graphql {
graalVm {
packages = listOf("com.expediagroup.graalvm.ktor")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/*
* 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.graalvm.ktor

import com.expediagroup.graalvm.ktor.context.CustomContextFactory
import com.expediagroup.graalvm.ktor.hooks.CustomHooks
import com.expediagroup.graalvm.ktor.schema.ArgumentQuery
import com.expediagroup.graalvm.ktor.schema.AsyncQuery
import com.expediagroup.graalvm.ktor.schema.BasicMutation
import com.expediagroup.graalvm.ktor.schema.ContextualQuery
import com.expediagroup.graalvm.ktor.schema.CustomScalarQuery
import com.expediagroup.graalvm.ktor.schema.EnumQuery
import com.expediagroup.graalvm.ktor.schema.ErrorQuery
import com.expediagroup.graalvm.ktor.schema.IdQuery
import com.expediagroup.graalvm.ktor.schema.model.ExampleInterface
import com.expediagroup.graalvm.ktor.schema.model.ExampleUnion
import com.expediagroup.graalvm.ktor.schema.InnerClassQuery
import com.expediagroup.graalvm.ktor.schema.ListQuery
import com.expediagroup.graalvm.ktor.schema.PolymorphicQuery
import com.expediagroup.graalvm.ktor.schema.ScalarQuery
import com.expediagroup.graalvm.ktor.schema.TypesQuery
import com.expediagroup.graalvm.ktor.schema.dataloader.ExampleDataLoader
import com.expediagroup.graalvm.ktor.schema.model.FirstImpl
import com.expediagroup.graalvm.ktor.schema.model.FirstUnionMember
import com.expediagroup.graalvm.ktor.schema.model.SecondImpl
import com.expediagroup.graalvm.ktor.schema.model.SecondUnionMember
import com.expediagroup.graphql.dataloader.KotlinDataLoaderRegistryFactory
import com.expediagroup.graphql.server.ktor.GraphQL
import com.expediagroup.graphql.server.ktor.graphQLGetRoute
import com.expediagroup.graphql.server.ktor.graphQLPostRoute
import com.expediagroup.graphql.server.ktor.graphQLSDLRoute
import com.expediagroup.graphql.server.ktor.graphiQLRoute
import io.ktor.server.application.install
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.routing.routing

fun main() {
embeddedServer(CIO, port = 8080, host = "0.0.0.0") {
install(GraphQL) {
schema {
packages = listOf("com.expediagroup.graalvm.ktor")
queries = listOf(
ArgumentQuery(),
AsyncQuery(),
ContextualQuery(),
CustomScalarQuery(),
EnumQuery(),
ErrorQuery(),
IdQuery(),
InnerClassQuery(),
ListQuery(),
PolymorphicQuery(),
ScalarQuery(),
TypesQuery()
)
mutations = listOf(
BasicMutation()
)
hooks = CustomHooks()
typeHierarchy = mapOf(
ExampleInterface::class to listOf(FirstImpl::class, SecondImpl::class),
ExampleUnion::class to listOf(FirstUnionMember::class, SecondUnionMember::class)
)
}
engine {
dataLoaderRegistryFactory = KotlinDataLoaderRegistryFactory(
ExampleDataLoader
)
}
server {
contextFactory = CustomContextFactory()
}
}
routing {
graphQLGetRoute()
graphQLPostRoute()
graphQLSDLRoute()
graphiQLRoute()
}
}.start(wait = true)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* 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.graalvm.ktor.context

import com.expediagroup.graphql.generator.extensions.plus
import com.expediagroup.graphql.server.ktor.DefaultKtorGraphQLContextFactory
import graphql.GraphQLContext
import io.ktor.server.request.ApplicationRequest
import java.util.UUID

class CustomContextFactory : DefaultKtorGraphQLContextFactory() {
override suspend fun generateContext(request: ApplicationRequest): GraphQLContext {
return super.generateContext(request).plus(mapOf("custom" to (request.headers["X-Custom-Header"] ?: UUID.randomUUID().toString())))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* 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.graalvm.ktor.hooks

import com.expediagroup.graphql.generator.hooks.SchemaGeneratorHooks
import com.expediagroup.graphql.plugin.schema.hooks.SchemaGeneratorHooksProvider
import graphql.language.StringValue
import graphql.schema.Coercing
import graphql.schema.CoercingParseLiteralException
import graphql.schema.CoercingParseValueException
import graphql.schema.CoercingSerializeException
import graphql.schema.GraphQLScalarType
import graphql.schema.GraphQLType
import java.util.UUID
import kotlin.reflect.KClass
import kotlin.reflect.KType

class CustomHooks : SchemaGeneratorHooks {
override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) {
UUID::class -> graphqlUUIDType
else -> null
}
}

class CustomHooksProvider : SchemaGeneratorHooksProvider {
override fun hooks(): SchemaGeneratorHooks = CustomHooks()
}

val graphqlUUIDType: GraphQLScalarType = GraphQLScalarType.newScalar()
.name("UUID")
.description("A type representing a formatted java.util.UUID")
.coercing(UUIDCoercing)
.build()

object UUIDCoercing : Coercing<UUID, String> {
override fun parseValue(input: Any): UUID = runCatching {
UUID.fromString(serialize(input))
}.getOrElse {
throw CoercingParseValueException("Expected valid UUID but was $input")
}

override fun parseLiteral(input: Any): UUID {
val uuidString = (input as? StringValue)?.value
return runCatching {
UUID.fromString(uuidString)
}.getOrElse {
throw CoercingParseLiteralException("Expected valid UUID literal but was $uuidString")
}
}

override fun serialize(dataFetcherResult: Any): String = runCatching {
dataFetcherResult.toString()
}.getOrElse {
throw CoercingSerializeException("Data fetcher result $dataFetcherResult cannot be serialized to a String")
}
}
Loading

0 comments on commit 2902678

Please sign in to comment.