From affa1d70b620fdf47b110d2604cd17ce6ebf4eea Mon Sep 17 00:00:00 2001 From: Thomas McKernan Date: Wed, 14 May 2025 11:10:21 -0500 Subject: [PATCH 1/3] add grpc server kotlin tests Signed-off-by: Thomas McKernan --- samples/grpc-server-kotlin/HELP-gradle.md | 71 ++++ samples/grpc-server-kotlin/HELP-maven.md | 71 ++++ samples/grpc-server-kotlin/README.md | 40 +++ samples/grpc-server-kotlin/build.gradle | 83 +++++ samples/grpc-server-kotlin/pom.xml | 264 ++++++++++++++ .../grpc/sample/GrpcServerApplication.kt | 25 ++ .../grpc/sample/GrpcServerService.kt | 33 ++ .../src/main/proto/hello.proto | 23 ++ .../src/main/resources/application.properties | 1 + .../grpc/sample/GrpcClientApplicationTests.kt | 84 +++++ .../grpc/sample/GrpcServerApplicationTests.kt | 51 +++ .../GrpcServerHealthIntegrationTests.kt | 169 +++++++++ .../grpc/sample/GrpcServerIntegrationTests.kt | 332 ++++++++++++++++++ .../test/resources/application-ssl.properties | 5 + .../src/test/resources/test.jks | Bin 0 -> 2264 bytes samples/pom.xml | 1 + samples/settings.gradle | 1 + 17 files changed, 1254 insertions(+) create mode 100644 samples/grpc-server-kotlin/HELP-gradle.md create mode 100644 samples/grpc-server-kotlin/HELP-maven.md create mode 100644 samples/grpc-server-kotlin/README.md create mode 100644 samples/grpc-server-kotlin/build.gradle create mode 100644 samples/grpc-server-kotlin/pom.xml create mode 100644 samples/grpc-server-kotlin/src/main/kotlin/org/springframework/grpc/sample/GrpcServerApplication.kt create mode 100644 samples/grpc-server-kotlin/src/main/kotlin/org/springframework/grpc/sample/GrpcServerService.kt create mode 100644 samples/grpc-server-kotlin/src/main/proto/hello.proto create mode 100644 samples/grpc-server-kotlin/src/main/resources/application.properties create mode 100644 samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcClientApplicationTests.kt create mode 100644 samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerApplicationTests.kt create mode 100644 samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerHealthIntegrationTests.kt create mode 100644 samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerIntegrationTests.kt create mode 100644 samples/grpc-server-kotlin/src/test/resources/application-ssl.properties create mode 100644 samples/grpc-server-kotlin/src/test/resources/test.jks diff --git a/samples/grpc-server-kotlin/HELP-gradle.md b/samples/grpc-server-kotlin/HELP-gradle.md new file mode 100644 index 00000000..3dd84be2 --- /dev/null +++ b/samples/grpc-server-kotlin/HELP-gradle.md @@ -0,0 +1,71 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Gradle documentation](https://docs.gradle.org) +* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/3.4.5/gradle-plugin) +* [Create an OCI image](https://docs.spring.io/spring-boot/3.4.5/gradle-plugin/packaging-oci-image.html) +* [GraalVM Native Image Support](https://docs.spring.io/spring-boot/3.4.5/reference/packaging/native-image/introducing-graalvm-native-images.html) +* [Spring gRPC [Experimental]](https://docs.spring.io/spring-grpc/reference/index.html) + +### Additional Links +These additional references should also help you: + +* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) +* [Configure AOT settings in Build Plugin](https://docs.spring.io/spring-boot/3.4.4/how-to/aot.html) +* [Various sample apps using Spring gRPC](https://github.com/spring-projects/spring-grpc/tree/main/samples) + +## GraalVM Native Support + +This project has been configured to let you generate either a lightweight container or a native executable. +It is also possible to run your tests in a native image. + +### Lightweight Container with Cloud Native Buildpacks +If you're already familiar with Spring Boot container images support, this is the easiest way to get started. +Docker should be installed and configured on your machine prior to creating the image. + +To create the image, run the following goal: + +``` +$ ./gradlew bootBuildImage +``` + +Then, you can run the app like any other container: + +``` +$ docker run --rm grpc-server:0.5.0-SNAPSHOT +``` + +### Executable with Native Build Tools +Use this option if you want to explore more options such as running your tests in a native image. +The GraalVM `native-image` compiler should be installed and configured on your machine. + +NOTE: GraalVM 22.3+ is required. + +To create the executable, run the following goal: + +``` +$ ./gradlew nativeCompile +``` + +Then, you can run the app as follows: +``` +$ build/native/nativeCompile/grpc-server +``` + +You can also run your existing tests suite in a native image. +This is an efficient way to validate the compatibility of your application. + +To run your existing tests in a native image, run the following goal: + +``` +$ ./gradlew nativeTest +``` + +### Gradle Toolchain support + +There are some limitations regarding Native Build Tools and Gradle toolchains. +Native Build Tools disable toolchain support by default. +Effectively, native image compilation is done with the JDK used to execute Gradle. +You can read more about [toolchain support in the Native Build Tools here](https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html#configuration-toolchains). diff --git a/samples/grpc-server-kotlin/HELP-maven.md b/samples/grpc-server-kotlin/HELP-maven.md new file mode 100644 index 00000000..a1d90765 --- /dev/null +++ b/samples/grpc-server-kotlin/HELP-maven.md @@ -0,0 +1,71 @@ +# Getting Started + +### Reference Documentation +For further reference, please consider the following sections: + +* [Official Apache Maven documentation](https://maven.apache.org/guides/index.html) +* [Spring Boot Maven Plugin Reference Guide](https://docs.spring.io/spring-boot/3.4.5/maven-plugin) +* [Create an OCI image](https://docs.spring.io/spring-boot/3.4.5/maven-plugin/build-image.html) +* [GraalVM Native Image Support](https://docs.spring.io/spring-boot/3.4.5/reference/packaging/native-image/introducing-graalvm-native-images.html) +* [Spring gRPC [Experimental]](https://docs.spring.io/spring-grpc/reference/index.html) + +### Additional Links +These additional references should also help you: + +* [Configure AOT settings in Build Plugin](https://docs.spring.io/spring-boot/3.4.4/how-to/aot.html) +* [Various sample apps using Spring gRPC](https://github.com/spring-projects/spring-grpc/tree/main/samples) + +## GraalVM Native Support + +This project has been configured to let you generate either a lightweight container or a native executable. +It is also possible to run your tests in a native image. + +### Lightweight Container with Cloud Native Buildpacks +If you're already familiar with Spring Boot container images support, this is the easiest way to get started. +Docker should be installed and configured on your machine prior to creating the image. + +To create the image, run the following goal: + +``` +$ ./mvnw spring-boot:build-image -Pnative +``` + +Then, you can run the app like any other container: + +``` +$ docker run --rm grpc-server-sample:0.5.0-SNAPSHOT +``` + +### Executable with Native Build Tools +Use this option if you want to explore more options such as running your tests in a native image. +The GraalVM `native-image` compiler should be installed and configured on your machine. + +NOTE: GraalVM 22.3+ is required. + +To create the executable, run the following goal: + +``` +$ ./mvnw native:compile -Pnative +``` + +Then, you can run the app as follows: +``` +$ target/grpc-server-sample +``` + +You can also run your existing tests suite in a native image. +This is an efficient way to validate the compatibility of your application. + +To run your existing tests in a native image, run the following goal: + +``` +$ ./mvnw test -PnativeTest +``` + + +### Maven Parent overrides + +Due to Maven's design, elements are inherited from the parent POM to the project POM. +While most of the inheritance is fine, it also inherits unwanted elements like `` and `` from the parent. +To prevent this, the project POM contains empty overrides for these elements. +If you manually switch to a different parent and actually want the inheritance, you need to remove those overrides. diff --git a/samples/grpc-server-kotlin/README.md b/samples/grpc-server-kotlin/README.md new file mode 100644 index 00000000..5304c4ee --- /dev/null +++ b/samples/grpc-server-kotlin/README.md @@ -0,0 +1,40 @@ +# Spring Boot gRPC Sample + +This project is a copy one of the samples from the [gRPC Spring Boot Starter](https://github.com/yidongnan/grpc-spring-boot-starter/blob/master/examples/local-grpc-server/build.gradle). Build and run any way you like to run Spring Boot. E.g: + +``` +$ ./mvnw spring-boot:run +... + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + :: Spring Boot :: (v3.4.5) +2022-12-08T05:32:24.934-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : Starting DemoApplication using Java 17.0.5 with PID 551632 (/home/dsyer/dev/scratch/demo/target/classes started by dsyer in /home/dsyer/dev/scratch/demo) +2022-12-08T05:32:24.938-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : No active profile set, falling back to 1 default profile: "default" +2022-12-08T05:32:25.377-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcHealthServiceAutoConfiguration +2022-12-08T05:32:25.416-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcServerAutoConfiguration +2022-12-08T05:32:25.425-08:00 WARN 551632 --- [ main] ocalVariableTableParameterNameDiscoverer : Using deprecated '-debug' fallback for parameter name resolution. Compile the affected code with '-parameters' instead or avoid its introspection: net.devh.boot.grpc.server.autoconfigure.GrpcServerFactoryAutoConfiguration +2022-12-08T05:32:25.427-08:00 INFO 551632 --- [ main] g.s.a.GrpcServerFactoryAutoConfiguration : Detected grpc-netty: Creating NettyGrpcServerFactory +2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: Simple, bean: grpcServerService, class: com.example.demo.GrpcServerService +2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.health.v1.Health, bean: grpcHealthService, class: io.grpc.protobuf.services.HealthServiceImpl +2022-12-08T05:32:25.712-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.AbstractGrpcServerFactory : Registered gRPC service: grpc.reflection.v1alpha.ServerReflection, bean: protoReflectionService, class: io.grpc.protobuf.services.ProtoReflectionService +2022-12-08T05:32:25.820-08:00 INFO 551632 --- [ main] n.d.b.g.s.s.GrpcServerLifecycle : gRPC Server started, listening on address: *, port: 9090 +2022-12-08T05:32:25.831-08:00 INFO 551632 --- [ main] com.example.demo.DemoApplication : Started DemoApplication in 1.264 seconds (process running for 1.623) +``` + +The server starts by default on port 9090. Test with [gRPCurl](https://github.com/fullstorydev/grpcurl): + +``` +$ grpcurl -d '{"name":"Hi"}' -plaintext localhost:9090 Simple.SayHello +{ + "message": "Hello ==\u003e Hi" +} +``` + +## Native Image +[Native Image with Gradle](./HELP-gradle.md) + +[Native Image with Maven](./HELP-maven.md) diff --git a/samples/grpc-server-kotlin/build.gradle b/samples/grpc-server-kotlin/build.gradle new file mode 100644 index 00000000..012f1978 --- /dev/null +++ b/samples/grpc-server-kotlin/build.gradle @@ -0,0 +1,83 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.4.5' + id 'io.spring.dependency-management' version '1.1.6' +// id 'org.graalvm.buildtools.native' version '0.10.3' + id 'com.google.protobuf' version '0.9.4' + id 'org.jetbrains.kotlin.jvm' version '2.1.20' + id 'org.jetbrains.kotlin.plugin.spring' version '2.1.20' +} + +group = 'com.example' +version = '0.9.0-SNAPSHOT' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() + maven { url 'https://repo.spring.io/milestone' } + maven { url 'https://repo.spring.io/snapshot' } +} + +dependencyManagement { + imports { + mavenBom 'org.springframework.grpc:spring-grpc-dependencies:0.9.0-SNAPSHOT' + } +} + +def kotlinStubVersion = "1.4.3" + +dependencies { + + implementation 'org.springframework.grpc:spring-grpc-spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'io.grpc:grpc-services' + implementation "io.grpc:grpc-kotlin-stub:${kotlinStubVersion}" + implementation 'com.google.protobuf:protobuf-kotlin' + + implementation "org.jetbrains.kotlin:kotlin-reflect" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.grpc:spring-grpc-test' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + testRuntimeOnly "io.netty:netty-transport-native-epoll::linux-x86_64" +} + +test { + useJUnitPlatform() + testLogging.showStandardStreams = true + outputs.upToDateWhen { false } +} + + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:${dependencyManagement.importedProperties['protobuf-java.version']}" + } + plugins { + grpc { + artifact = "io.grpc:protoc-gen-grpc-java:${dependencyManagement.importedProperties['grpc.version']}" + } + grpckt { + artifact = "io.grpc:protoc-gen-grpc-kotlin:${kotlinStubVersion}:jdk8@jar" + } + } + generateProtoTasks { + all()*.plugins { + grpc { + option 'jakarta_omit' + option '@generated=omit' + } + grpckt { + outputSubDir = "kotlin" + } + } + } +} + diff --git a/samples/grpc-server-kotlin/pom.xml b/samples/grpc-server-kotlin/pom.xml new file mode 100644 index 00000000..c7de7bd3 --- /dev/null +++ b/samples/grpc-server-kotlin/pom.xml @@ -0,0 +1,264 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.5 + + + org.springframework.grpc + grpc-server-kotlin-sample + 0.9.0-SNAPSHOT + Spring gRPC Server Sample + Demo project for Spring gRPC + + + + + + + + + + + + + + + 17 + 0.0.43 + 4.30.2 + 1.72.0 + 1.4.3 + 1.9.22 + + + + + org.springframework.grpc + spring-grpc-dependencies + 0.9.0-SNAPSHOT + pom + import + + + + + + org.springframework.grpc + spring-grpc-spring-boot-starter + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + 1.10.2 + + + io.grpc + grpc-kotlin-stub + ${grpc.kotlin.version} + + + com.google.protobuf + protobuf-kotlin + + + io.grpc + grpc-services + + + org.springframework.boot + spring-boot-starter-actuator + + + + + io.netty + netty-transport-native-epoll + linux-x86_64 + test + + + org.springframework.grpc + spring-grpc-test + test + + + org.jetbrains + annotations + 24.0.1 + compile + + + org.jetbrains + annotations + 24.0.1 + compile + + + org.jetbrains + annotations + 24.0.1 + compile + + + + + src/main/kotlin + + + kr.motd.maven + os-maven-plugin + 1.7.1 + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + + compile + + + + test-compile + + test-compile + + + + + + org.graalvm.buildtools + native-maven-plugin + + + --verbose + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + io.spring.javaformat + spring-javaformat-maven-plugin + ${spring-javaformat-maven-plugin.version} + + + + validate + true + + validate + + + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + 0.6.1 + + + com.google.protobuf:protoc:${protobuf-java.version}:exe:${os.detected.classifier} + + grpc-java + + io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + + grpc-kotlin + io.grpc + protoc-gen-grpc-kotlin + ${grpc.kotlin.version} + jdk8 + org.springframework.grpc.sample.GrpcServerApplicationKt + + + + + + + + jakarta_omit,@generated=omit + + + + compile + compile-custom + + + + grpc-kotlin + + compile-custom + + + grpc-kotlin + + + + + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-snapshots + Spring Snapshots + https://repo.spring.io/snapshot + + false + + + + + + diff --git a/samples/grpc-server-kotlin/src/main/kotlin/org/springframework/grpc/sample/GrpcServerApplication.kt b/samples/grpc-server-kotlin/src/main/kotlin/org/springframework/grpc/sample/GrpcServerApplication.kt new file mode 100644 index 00000000..bef90f01 --- /dev/null +++ b/samples/grpc-server-kotlin/src/main/kotlin/org/springframework/grpc/sample/GrpcServerApplication.kt @@ -0,0 +1,25 @@ +package org.springframework.grpc.sample + +import io.grpc.Status +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.context.annotation.Bean +import org.springframework.grpc.server.exception.GrpcExceptionHandler + +@SpringBootApplication +open class GrpcServerApplication { + + @Bean + open fun globalInterceptor(): GrpcExceptionHandler = GrpcExceptionHandler { exception -> + when (exception) { + is IllegalArgumentException -> Status.INVALID_ARGUMENT.withDescription(exception.message).asException(); + else -> null + } + } + +} + + +fun main(args: Array) { + runApplication(*args) +} diff --git a/samples/grpc-server-kotlin/src/main/kotlin/org/springframework/grpc/sample/GrpcServerService.kt b/samples/grpc-server-kotlin/src/main/kotlin/org/springframework/grpc/sample/GrpcServerService.kt new file mode 100644 index 00000000..cb8a29a4 --- /dev/null +++ b/samples/grpc-server-kotlin/src/main/kotlin/org/springframework/grpc/sample/GrpcServerService.kt @@ -0,0 +1,33 @@ +package org.springframework.grpc.sample + +import org.springframework.grpc.sample.proto.HelloReply +import org.springframework.grpc.sample.proto.HelloRequest +import org.springframework.grpc.sample.proto.SimpleGrpcKt +import org.springframework.stereotype.Service +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow + +@Service +class GrpcServerService : SimpleGrpcKt.SimpleCoroutineImplBase() { + + override suspend fun sayHello(request: HelloRequest): HelloReply { + + if (request.name.startsWith("error")) { + throw IllegalArgumentException("Bad name: ${request.name}") + } + + if (request.name.startsWith("internal")) { + throw RuntimeException() + } + + return HelloReply.newBuilder() + .setMessage("Hello ==> ${request.name}") + .build() + } + + override fun streamHello(request: HelloRequest): Flow { + return (1..10) + .map { HelloReply.newBuilder().setMessage("Hello ($it) ==> ${request.name}").build() } + .asFlow() + } +} diff --git a/samples/grpc-server-kotlin/src/main/proto/hello.proto b/samples/grpc-server-kotlin/src/main/proto/hello.proto new file mode 100644 index 00000000..731679cd --- /dev/null +++ b/samples/grpc-server-kotlin/src/main/proto/hello.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "org.springframework.grpc.sample.proto"; +option java_outer_classname = "HelloWorldProto"; + +// The greeting service definition. +service Simple { + // Sends a greeting + rpc SayHello (HelloRequest) returns (HelloReply) { + } + rpc StreamHello(HelloRequest) returns (stream HelloReply) {} +} + +// The request message containing the user's name. +message HelloRequest { + string name = 1; +} + +// The response message containing the greetings +message HelloReply { + string message = 1; +} \ No newline at end of file diff --git a/samples/grpc-server-kotlin/src/main/resources/application.properties b/samples/grpc-server-kotlin/src/main/resources/application.properties new file mode 100644 index 00000000..27d75ad1 --- /dev/null +++ b/samples/grpc-server-kotlin/src/main/resources/application.properties @@ -0,0 +1 @@ +spring.application.name=grpc-server-kotlin diff --git a/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcClientApplicationTests.kt b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcClientApplicationTests.kt new file mode 100644 index 00000000..ab5671ef --- /dev/null +++ b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcClientApplicationTests.kt @@ -0,0 +1,84 @@ +package org.springframework.grpc.sample + +import io.grpc.stub.AbstractStub +import org.assertj.core.api.Assertions +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.ApplicationContext +import org.springframework.grpc.client.FutureStubFactory +import org.springframework.grpc.client.ImportGrpcClients +import org.springframework.grpc.sample.proto.SimpleGrpc +import org.springframework.grpc.sample.proto.SimpleGrpc.SimpleBlockingStub +import org.springframework.grpc.sample.proto.SimpleGrpc.SimpleFutureStub +import org.springframework.grpc.test.AutoConfigureInProcessTransport + + +@SpringBootTest +@AutoConfigureInProcessTransport +internal class NoAutowiredClients { + + @Autowired + private lateinit var context: ApplicationContext + + @Test + fun noStubIsCreated() { + assertThat(context.containsBeanDefinition("simpleBlockingStub")).isFalse() + assertThat(context.containsBeanDefinition("simpleStub")).isFalse() + assertThat(context.containsBeanDefinition("simpleFutureStub")).isFalse() + assertThat(context.getBeanNamesForType(AbstractStub::class.java)) + .isEmpty() + } +} + +@SpringBootTest(properties = ["spring.grpc.client.default-channel.address=0.0.0.0:9090"]) +@AutoConfigureInProcessTransport +internal class DefaultAutowiredClients { + @Autowired + private lateinit var context: ApplicationContext + + @Test + fun onlyDefaultStubIsCreated() { + Assertions.assertThat(context.containsBeanDefinition("simpleBlockingStub")).isTrue() + assertThat( + context.getBean( + SimpleBlockingStub::class.java + ) + ).isNotNull() + assertThat(context.containsBeanDefinition("simpleStub")).isFalse() + assertThat(context.containsBeanDefinition("simpleFutureStub")).isFalse() + assertThat(context.getBeanNamesForType(AbstractStub::class.java)) + .hasSize(1) + } +} + +@SpringBootTest( + properties = ["spring.grpc.client.default-channel.address=0.0.0.0:9090"] +) +@AutoConfigureInProcessTransport +internal class SpecificAutowiredClients { + @Autowired + private lateinit var context: ApplicationContext + + @Test + fun stubOfCorrectTypeIsCreated() { + assertThat(context.containsBeanDefinition("simpleFutureStub")).isTrue() + assertThat( + context.getBean( + SimpleFutureStub::class.java + ) + ).isNotNull() + assertThat(context.containsBeanDefinition("simpleStub")).isFalse() + assertThat(context.containsBeanDefinition("simpleBlockingStub")).isFalse() + assertThat(context.getBeanNamesForType(AbstractStub::class.java)) + .hasSize(1) + } + + @TestConfiguration + @ImportGrpcClients(basePackageClasses = [SimpleGrpc::class], factory = FutureStubFactory::class) + internal open class TestConfig +} + diff --git a/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerApplicationTests.kt b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerApplicationTests.kt new file mode 100644 index 00000000..319c9a32 --- /dev/null +++ b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerApplicationTests.kt @@ -0,0 +1,51 @@ +package org.springframework.grpc.sample + +import org.apache.commons.logging.Log +import org.apache.commons.logging.LogFactory +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.builder.SpringApplicationBuilder +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.SpringBootTest.UseMainMethod +import org.springframework.grpc.sample.proto.HelloRequest +import org.springframework.grpc.sample.proto.SimpleGrpc.SimpleBlockingStub +import org.springframework.test.annotation.DirtiesContext + +@SpringBootTest( + properties = [ + "spring.grpc.server.port=0", + "spring.grpc.client.default-channel.address=0.0.0.0:\${local.grpc.port}" + ], + useMainMethod = UseMainMethod.ALWAYS +) +@DirtiesContext +class GrpcServerApplicationTests { + + @Autowired + private lateinit var stub: SimpleBlockingStub + + @Test + fun contextLoads() { + } + + @Test + fun serverResponds() { + log.info("Testing") + val response = stub.sayHello( + HelloRequest.newBuilder() + .setName("Alien") + .build() + ) + Assertions.assertEquals("Hello ==> Alien", response.getMessage()) + } + + companion object { + private val log: Log = LogFactory.getLog(GrpcServerApplicationTests::class.java) + + @JvmStatic + fun main(args: Array) { + SpringApplicationBuilder(GrpcServerApplication::class.java).run() + } + } +} diff --git a/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerHealthIntegrationTests.kt b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerHealthIntegrationTests.kt new file mode 100644 index 00000000..d0be3aaf --- /dev/null +++ b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerHealthIntegrationTests.kt @@ -0,0 +1,169 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * 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 org.springframework.grpc.sample + +import io.grpc.StatusRuntimeException +import io.grpc.health.v1.HealthCheckRequest +import io.grpc.health.v1.HealthCheckResponse.ServingStatus +import io.grpc.health.v1.HealthGrpc +import io.grpc.health.v1.HealthGrpc.HealthBlockingStub +import io.grpc.protobuf.services.HealthStatusManager +import org.assertj.core.api.Assertions +import org.assertj.core.api.ThrowableAssert +import org.awaitility.Awaitility +import org.awaitility.core.ThrowingRunnable +import org.junit.jupiter.api.Nested +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.actuate.autoconfigure.health.ConditionalOnEnabledHealthIndicator +import org.springframework.boot.actuate.health.Health +import org.springframework.boot.actuate.health.HealthIndicator +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.grpc.client.GrpcChannelFactory +import org.springframework.grpc.sample.proto.HelloRequest +import org.springframework.grpc.sample.proto.SimpleGrpc +import org.springframework.grpc.sample.proto.SimpleGrpc.SimpleBlockingStub +import org.springframework.grpc.test.AutoConfigureInProcessTransport +import org.springframework.test.annotation.DirtiesContext +import java.time.Duration + +/** + * Integration tests for gRPC server health feature. + */ + +@SpringBootTest( + properties = ["spring.grpc.server.port=0", "spring.grpc.client.channels.health-test.address=static://0.0.0.0:\${local.grpc.port}", "spring.grpc.client.channels.health-test.health.enabled=true", "spring.grpc.client.channels.health-test.health.service-name=my-service"] +) +@DirtiesContext +internal class WithClientHealthEnabled { + @Test + fun loadBalancerRespectsServerHealth( + @Autowired channels: GrpcChannelFactory, + @Autowired healthStatusManager: HealthStatusManager + ) { + val channel = channels.createChannel("health-test") + val client = SimpleGrpc.newBlockingStub(channel) + + // put the service up (SERVING) and give load balancer time to update + updateHealthStatusAndWait("my-service", ServingStatus.SERVING, healthStatusManager) + + // initially the status should be SERVING + assertThatResponseIsServedToChannel(client) + + // put the service down (NOT_SERVING) and give load balancer time to update + updateHealthStatusAndWait("my-service", ServingStatus.NOT_SERVING, healthStatusManager) + + // now the request should fail + assertThatResponseIsNotServedToChannel(client) + + // put the service up (SERVING) and give load balancer time to update + updateHealthStatusAndWait("my-service", ServingStatus.SERVING, healthStatusManager) + + // now the request should pass + assertThatResponseIsServedToChannel(client) + } + + private fun updateHealthStatusAndWait( + serviceName: String?, healthStatus: ServingStatus, + healthStatusManager: HealthStatusManager + ) { + healthStatusManager.setStatus(serviceName, healthStatus) + try { + Thread.sleep(2000L) + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + } + + private fun assertThatResponseIsServedToChannel(client: SimpleBlockingStub) { + val response = client.sayHello(HelloRequest.newBuilder().setName("Alien").build()) + Assertions.assertThat(response.getMessage()).isEqualTo("Hello ==> Alien") + } + + private fun assertThatResponseIsNotServedToChannel(client: SimpleBlockingStub) { + Assertions.assertThatExceptionOfType(StatusRuntimeException::class.java) + .isThrownBy(ThrowableAssert.ThrowingCallable { + client.sayHello( + HelloRequest.newBuilder().setName("Alien").build() + ) + }) + .withMessageContaining("UNAVAILABLE: Health-check service responded NOT_SERVING for 'my-service'") + } +} + + +@SpringBootTest( + properties = ["spring.grpc.server.health.actuator.health-indicator-paths=custom", "spring.grpc.server.health.actuator.update-initial-delay=3s", "spring.grpc.server.health.actuator.update-rate=3s", "management.health.defaults.enabled=true"] +) +@AutoConfigureInProcessTransport +@DirtiesContext +internal class WithActuatorHealthAdapter { + @Test + fun healthIndicatorsAdaptedToGrpcHealthStatus( + @Autowired channels: GrpcChannelFactory, + @Autowired customHealthIndicator: CustomHealthIndicator + ) { + val channel = channels.createChannel("0.0.0.0:0") + val healthStub = HealthGrpc.newBlockingStub(channel) + val serviceName = "custom" + + // initially the status should be SERVING + assertThatGrpcHealthStatusIs(healthStub, serviceName, ServingStatus.SERVING, Duration.ofSeconds(4)) + + // put the service down and the status should then be NOT_SERVING + customHealthIndicator.SERVICE_IS_UP = false + assertThatGrpcHealthStatusIs(healthStub, serviceName, ServingStatus.NOT_SERVING, Duration.ofSeconds(4)) + + // put the service up and the status should be SERVING + customHealthIndicator.SERVICE_IS_UP = true + assertThatGrpcHealthStatusIs(healthStub, serviceName, ServingStatus.SERVING, Duration.ofSeconds(4)) + } + + private fun assertThatGrpcHealthStatusIs( + healthBlockingStub: HealthBlockingStub, service: String, + expectedStatus: ServingStatus?, maxWaitTime: Duration? + ) { + Awaitility.await().atMost(maxWaitTime).ignoreException(StatusRuntimeException::class.java).untilAsserted { + val healthRequest = HealthCheckRequest.newBuilder().setService(service).build() + val healthResponse = healthBlockingStub.check(healthRequest) + Assertions.assertThat(healthResponse.getStatus()).isEqualTo(expectedStatus) + // verify the overall status as well + val overallHealthRequest = HealthCheckRequest.newBuilder().setService("").build() + val overallHealthResponse = healthBlockingStub.check(overallHealthRequest) + Assertions.assertThat(overallHealthResponse.getStatus()).isEqualTo(expectedStatus) + } + } + + @TestConfiguration + internal open class MyHealthIndicatorsConfig { + @ConditionalOnEnabledHealthIndicator("custom") + @Bean + open fun customHealthIndicator(): CustomHealthIndicator { + return CustomHealthIndicator() + } + } + + internal class CustomHealthIndicator : HealthIndicator { + override fun health(): Health? { + return if (SERVICE_IS_UP) Health.up().build() else Health.down().build() + } + + var SERVICE_IS_UP: Boolean = true + } +} + diff --git a/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerIntegrationTests.kt b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerIntegrationTests.kt new file mode 100644 index 00000000..f8b18a92 --- /dev/null +++ b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerIntegrationTests.kt @@ -0,0 +1,332 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * 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 org.springframework.grpc.sample + +import io.grpc.ForwardingServerCallListener +import io.grpc.ManagedChannel +import io.grpc.Metadata +import io.grpc.ServerCall +import io.grpc.ServerCallHandler +import io.grpc.ServerInterceptor +import io.grpc.Status +import io.grpc.StatusRuntimeException +import io.grpc.netty.NettyChannelBuilder +import org.assertj.core.api.Assertions +import org.junit.Assert +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.condition.EnabledOnOs +import org.junit.jupiter.api.condition.OS +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.context.TestConfiguration +import org.springframework.context.annotation.Bean +import org.springframework.grpc.client.ChannelBuilderOptions +import org.springframework.grpc.client.GrpcChannelBuilderCustomizer +import org.springframework.grpc.client.GrpcChannelFactory +import org.springframework.grpc.sample.proto.HelloRequest +import org.springframework.grpc.sample.proto.SimpleGrpc +import org.springframework.grpc.server.GlobalServerInterceptor +import org.springframework.grpc.test.AutoConfigureInProcessTransport +import org.springframework.grpc.test.LocalGrpcPort +import org.springframework.test.annotation.DirtiesContext +import org.springframework.test.context.ActiveProfiles +import java.util.concurrent.atomic.AtomicInteger + +/** + * More detailed integration tests for [gRPC server factories][GrpcServerFactory] and + * various [GrpcServerProperties]. + */ + +@SpringBootTest +@AutoConfigureInProcessTransport +internal class ServerWithInProcessChannel { + @Test + fun servesResponseToClient(@Autowired channels: GrpcChannelFactory) { + assertThatResponseIsServedToChannel(channels.createChannel("0.0.0.0:0")) + } +} + + +@SpringBootTest +@AutoConfigureInProcessTransport +internal class ServerWithException { + + @Test + fun specificErrorResponse(@Autowired channels: GrpcChannelFactory) { + val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")) + Assertions.assertThat( + Assert.assertThrows(StatusRuntimeException::class.java) { + client.sayHello( + HelloRequest.newBuilder().setName("error").build() + ) + } + .status + .code + ).isEqualTo(Status.Code.INVALID_ARGUMENT) + } + + @Test + fun defaultErrorResponseIsUnknown(@Autowired channels: GrpcChannelFactory) { + val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")) + Assertions.assertThat( + Assert.assertThrows( + StatusRuntimeException::class.java + ) { client.sayHello(HelloRequest.newBuilder().setName("internal").build()) } + .status + .code + ).isEqualTo(Status.Code.UNKNOWN) + } +} + + +@SpringBootTest +@AutoConfigureInProcessTransport +internal class ServerWithExceptionInInterceptorCall { + @Test + fun specificErrorResponse(@Autowired channels: GrpcChannelFactory) { + val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")) + Assertions.assertThat( + Assert.assertThrows( + StatusRuntimeException::class.java + ) { client.sayHello(HelloRequest.newBuilder().setName("foo").build()) } + .status + .code + ).isEqualTo(Status.Code.INVALID_ARGUMENT) + } + + @TestConfiguration + internal open class TestConfig { + @Bean + @GlobalServerInterceptor + open fun exceptionInterceptor(): ServerInterceptor { + return CustomInterceptor() + } + + internal class CustomInterceptor : ServerInterceptor { + override fun interceptCall( + call: ServerCall?, headers: Metadata?, + next: ServerCallHandler? + ): ServerCall.Listener? { + throw IllegalArgumentException("test") + } + } + } +} + + +@SpringBootTest +@AutoConfigureInProcessTransport +internal class ServerWithExceptionInInterceptorListener { + @Test + fun specificErrorResponse( + @Autowired channels: GrpcChannelFactory, + @Autowired testConfig: TestConfig + ) { + testConfig.reset() + val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")) + Assertions.assertThat( + Assert.assertThrows( + StatusRuntimeException::class.java + ) { client.sayHello(HelloRequest.newBuilder().setName("foo").build()) } + .status + .code + ).isEqualTo(Status.Code.INVALID_ARGUMENT) + Assertions.assertThat(TestConfig.readyCount.get()).isEqualTo(1) + Assertions.assertThat(TestConfig.callCount.get()).isEqualTo(0) + Assertions.assertThat(TestConfig.messageCount.get()).isEqualTo(0) + } + + @TestConfiguration + internal open class TestConfig { + companion object { + var callCount: AtomicInteger = AtomicInteger() + var messageCount: AtomicInteger = AtomicInteger() + var readyCount: AtomicInteger = AtomicInteger() + } + + fun reset() { + callCount.set(0) + messageCount.set(0) + readyCount.set(0) + } + + + @Bean + @GlobalServerInterceptor + open fun exceptionInterceptor(): ServerInterceptor { + return CustomInterceptor() + } + + internal class CustomInterceptor : ServerInterceptor { + override fun interceptCall( + call: ServerCall?, headers: Metadata?, + next: ServerCallHandler + ): ServerCall.Listener { + return CustomListener(next.startCall(call, headers)) + } + } + + internal class CustomListener(private val delegate: ServerCall.Listener?) : + ForwardingServerCallListener() { + override fun onReady() { + readyCount.incrementAndGet() + throw IllegalArgumentException("test") + } + + override fun onHalfClose() { + callCount.incrementAndGet() + super.onHalfClose() + } + + override fun onMessage(message: ReqT?) { + messageCount.incrementAndGet() + super.onMessage(message) + } + + override fun delegate(): ServerCall.Listener? { + return this.delegate + } + } + } +} + + +@SpringBootTest("spring.grpc.server.exception-handler.enabled=false") +@AutoConfigureInProcessTransport +internal class ServerWithUnhandledException { + @Test + fun specificErrorResponse(@Autowired channels: GrpcChannelFactory) { + val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")) + Assertions.assertThat( + Assert.assertThrows(StatusRuntimeException::class.java) { + client.sayHello( + HelloRequest.newBuilder().setName("error").build() + ) + } + .status + .code + ).isEqualTo(Status.Code.UNKNOWN) + } + + @Test + fun defaultErrorResponseIsUnknown(@Autowired channels: GrpcChannelFactory) { + val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")) + Assertions.assertThat( + Assert.assertThrows( + StatusRuntimeException::class.java + ) { client.sayHello(HelloRequest.newBuilder().setName("internal").build()) } + .status + .code + ).isEqualTo(Status.Code.UNKNOWN) + } +} + + +@SpringBootTest(properties = ["spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0"]) +internal class ServerWithAnyIPv4AddressAndRandomPort { + @Test + fun servesResponseToClientWithAnyIPv4AddressAndRandomPort( + @Autowired channels: GrpcChannelFactory, + @LocalGrpcPort port: Int + ) { + assertThatResponseIsServedToChannel(channels.createChannel("0.0.0.0:" + port)) + } +} + + +@SpringBootTest(properties = ["spring.grpc.server.host=::", "spring.grpc.server.port=0"]) +internal class ServerWithAnyIPv6AddressAndRandomPort { + @Test + fun servesResponseToClientWithAnyIPv4AddressAndRandomPort( + @Autowired channels: GrpcChannelFactory, + @LocalGrpcPort port: Int + ) { + assertThatResponseIsServedToChannel(channels.createChannel("0.0.0.0:" + port)) + } +} + + +@SpringBootTest(properties = ["spring.grpc.server.host=127.0.0.1", "spring.grpc.server.port=0"]) +internal class ServerWithLocalhostAndRandomPort { + @Test + fun servesResponseToClientWithLocalhostAndRandomPort( + @Autowired channels: GrpcChannelFactory, + @LocalGrpcPort port: Int + ) { + assertThatResponseIsServedToChannel(channels.createChannel("127.0.0.1:" + port)) + } +} + + +@SpringBootTest( + properties = ["spring.grpc.server.port=0", "spring.grpc.client.channels.test-channel.address=static://0.0.0.0:\${local.grpc.port}"] +) +@DirtiesContext +internal class ServerConfiguredWithStaticClientChannel { + @Test + fun servesResponseToClientWithConfiguredChannel(@Autowired channels: GrpcChannelFactory) { + assertThatResponseIsServedToChannel(channels.createChannel("test-channel")) + } +} + + +@SpringBootTest(properties = ["spring.grpc.server.address=unix:unix-test-channel"]) +@EnabledOnOs(OS.LINUX) +internal class ServerWithUnixDomain { + @Test + fun clientChannelWithUnixDomain(@Autowired channels: GrpcChannelFactory) { + assertThatResponseIsServedToChannel( + channels.createChannel( + "unix:unix-test-channel", + ChannelBuilderOptions.defaults() + .withCustomizer(GrpcChannelBuilderCustomizer { `__`: String?, b: NettyChannelBuilder? -> b!!.usePlaintext() }) + ) + ) + } +} + + +@SpringBootTest( + properties = ["spring.grpc.server.port=0", "spring.grpc.client.channels.test-channel.address=static://0.0.0.0:\${local.grpc.port}", "spring.grpc.client.channels.test-channel.negotiation-type=TLS", "spring.grpc.client.channels.test-channel.secure=false"] +) +@ActiveProfiles("ssl") +@DirtiesContext +internal class ServerWithSsl { + @Test + fun clientChannelWithSsl(@Autowired channels: GrpcChannelFactory) { + assertThatResponseIsServedToChannel(channels.createChannel("test-channel")) + } +} + + +@SpringBootTest( + properties = ["spring.grpc.server.port=0", "spring.grpc.server.ssl.client-auth=REQUIRE", "spring.grpc.server.ssl.secure=false", "spring.grpc.client.channels.test-channel.address=static://0.0.0.0:\${local.grpc.port}", "spring.grpc.client.channels.test-channel.ssl.bundle=ssltest", "spring.grpc.client.channels.test-channel.negotiation-type=TLS", "spring.grpc.client.channels.test-channel.secure=false"] +) +@ActiveProfiles("ssl") +@DirtiesContext +internal class ServerWithClientAuth { + @Test + fun clientChannelWithSsl(@Autowired channels: GrpcChannelFactory) { + assertThatResponseIsServedToChannel(channels.createChannel("test-channel")) + } +} + +private fun assertThatResponseIsServedToChannel(clientChannel: ManagedChannel?) { + val client = SimpleGrpc.newBlockingStub(clientChannel) + val response = client.sayHello(HelloRequest.newBuilder().setName("Alien").build()) + Assertions.assertThat(response.getMessage()).isEqualTo("Hello ==> Alien") +} + diff --git a/samples/grpc-server-kotlin/src/test/resources/application-ssl.properties b/samples/grpc-server-kotlin/src/test/resources/application-ssl.properties new file mode 100644 index 00000000..50ad5c4c --- /dev/null +++ b/samples/grpc-server-kotlin/src/test/resources/application-ssl.properties @@ -0,0 +1,5 @@ +spring.grpc.server.ssl.bundle=ssltest +spring.ssl.bundle.jks.ssltest.keystore.location=classpath:test.jks +spring.ssl.bundle.jks.ssltest.keystore.password=secret +spring.ssl.bundle.jks.ssltest.keystore.type=JKS +spring.ssl.bundle.jks.ssltest.key.password=password \ No newline at end of file diff --git a/samples/grpc-server-kotlin/src/test/resources/test.jks b/samples/grpc-server-kotlin/src/test/resources/test.jks new file mode 100644 index 0000000000000000000000000000000000000000..6aa9a28053a591e41453e665e5024e8a8cb78b3d GIT binary patch literal 2264 zcmchYX*3iJ7sqE|hQS!q5Mv)4GM2$i#uAFqC`%7x7baWA*i&dRX>3`uq(XS?3XSYp z%38`&ib7E$8j~$cF^}gt?|I+noW8#w?uYxk=iGD8|K9Vzd#pVc0002(2k@T|2@MMI zqxqr2AhQO*TVi`j@((S;e;g;l$#dAA{>vf0kX$R(Qn4oKgGEYjZ5zti2dw?Z6A zh%LuFCNI?9o+Z1duJL-++e#cjO`zlK?u9s030=k_*wD1#-$FbIDRDnA^vo@fm( zzjt(3VJrGOr0iHXSTM|rYN#>RZ@Dp`PwB2zrDQffLvuoR2~V3ReYa0&vU^dXd8isV zsAf*@!8s%xBvHLseXn6f?1kefe(8uAmAbaF$x{Ykzb6c6jdUwY1$y4tFzsj7 zIghr!T#ODfu@Po!a29@kXQ8kY#(LE<0o7?7PQ|eMeY@Equ?R-6*f@Na3o&stDQ=6( zQzDSQhCnS(9Bu9W_~giknP0vECqUsr4_9y_}nEU`cy z4}dApnAip92wMwgzciAFpc3i}+-#Zlq+iF7d1y}d4Qsp8=%l1N8NIs161I`HmkcpQ zY4*CUCFJJf(2!M{`&qQ}3($KeTQ=)mMrBs`DOb;%Of0tC)9he_p~w&CO#DfCgx(%s z{@|D(brX_Gb}ZDLmGej*JgEl0Et>q~kgTXuJg-PwvRjNx8sBbIShxD=xOySzw{;^X zAvrh5HTg>Xq@<{#^!Kg}B?qz@b<{ebD)yaSf&RChBIJQo-?Ahzw@qopSe^e&>^IuU zydM4Y1_C&>k7u|}=; z63R7$H6zat=hNExxEwXu1fQ*ytuEkP!{w{|#6TIEq1#*ck=6_NM*ILF65tmD-O5&R zMI!-MT<3U~t@}(CN4@RlZ~1I>C=!ywF)dNI{VvH;5Y3(Z4jY^%_c&fsm4Q`<1g|qX z&!h29jXjVE3nJnet*L)XL?-8<>qDbVGP%i^NwOZfwWO7?Mr!X7 zl}sG@9S_5}}td}$xrWIYY=e(VVBiv%A+M-{M z!3_^Tc=pV?niT!{D`!{e@W;MvrZ(OER{x7itVAtwE~spPtPtma|J=5dv&_oE!5H#` zdgXJ;+gJ4hI}*9QX9jpL`Gb)yCe%1}t!&O-^sihyZys%%5uF~WhsR_w(q7;vV5d4P zr%ZUA2}kO+L^2ePTgGT9Ua71w<+)poSyjTdLq&xbUn`<6&SpwFp(HRHUyU6J3WZ_! zfztko79+94Tq%mTYj53(RYcL&1~5`I#+w3`(Q|r+P(aT z%?r(^?IWw~19CB&uvXf(f7&BnEE{zwK4piVU`I4j1j?v5d4N<7VUJ8nM`$7S*mfKR z#9-JzPRZ?{M!@L+0N^V)IyeeP2T|^UK|m0QD+Ibs!wEoml^N!YO#vW~j~jraX(0A3 z6Kux?IRLez`O^X;{!4g%BhcRn>^H*qKZ3*|{_YGuz)KCJcu;)DSES5D2tDE`C02YR0R%Vy1T7k|RQ;3g<0icA$AuP0pOvc~jGl zz+NeKv_FT_;GWK&8XlDUv&hv9kxg?@c!bu?83i=YQ$S!K09Y)Glg3Hz?@|)ZCBlVz zP8i}#XZkMoje3I=h&I!!s_m?Qi@1MR`yv7X*yEs47qOs^t^?&=;*IQ!q&)gq_Sx5* z?fhU8Q*PSe*w7y)FH#P!9R^Xw!lTT+zI39L<&8cViaj$A(Z2Cg7!{V?uuyi#vlNCg z40i}2ivw&y&1-&Nh&WMG`&aIt>)(#tKTJ}^@696Kw1-{IzSOTnFF+0@k$o3%ZHS;Q#;t literal 0 HcmV?d00001 diff --git a/samples/pom.xml b/samples/pom.xml index bbe8e7c3..34880505 100644 --- a/samples/pom.xml +++ b/samples/pom.xml @@ -21,6 +21,7 @@ grpc-secure grpc-oauth2 grpc-reactive + grpc-server-kotlin grpc-server-netty-shaded grpc-tomcat grpc-tomcat-secure diff --git a/samples/settings.gradle b/samples/settings.gradle index 16b382c2..421ea61c 100644 --- a/samples/settings.gradle +++ b/samples/settings.gradle @@ -10,6 +10,7 @@ include 'grpc-reactive' include 'grpc-oauth2' include 'grpc-secure' include 'grpc-server' +include 'grpc-server-kotlin' include 'grpc-server-netty-shaded' include 'grpc-tomcat' include 'grpc-tomcat-secure' From 3a25b65ee3caabdbfcc8aae8d0c86894f504779f Mon Sep 17 00:00:00 2001 From: Thomas McKernan Date: Wed, 14 May 2025 11:26:24 -0500 Subject: [PATCH 2/3] fix main class Signed-off-by: Thomas McKernan --- samples/grpc-server-kotlin/pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/grpc-server-kotlin/pom.xml b/samples/grpc-server-kotlin/pom.xml index c7de7bd3..da74b489 100644 --- a/samples/grpc-server-kotlin/pom.xml +++ b/samples/grpc-server-kotlin/pom.xml @@ -12,7 +12,7 @@ org.springframework.grpc grpc-server-kotlin-sample 0.9.0-SNAPSHOT - Spring gRPC Server Sample + Spring gRPC Kotlin Server Sample Demo project for Spring gRPC @@ -193,7 +193,7 @@ protoc-gen-grpc-kotlin ${grpc.kotlin.version} jdk8 - org.springframework.grpc.sample.GrpcServerApplicationKt + io.grpc.kotlin.generator.GeneratorRunner From 56eeb70f31570df90a694a2f7c639e37955e4eb8 Mon Sep 17 00:00:00 2001 From: Thomas McKernan Date: Wed, 14 May 2025 19:06:10 -0500 Subject: [PATCH 3/3] add GrpcExceptionHandledServerCall to catch exceptions Signed-off-by: Thomas McKernan --- README.md | 3 +- samples/grpc-server-kotlin/pom.xml | 20 ++++---- .../grpc/sample/GrpcClientApplicationTests.kt | 6 +-- .../GrpcServerHealthIntegrationTests.kt | 6 +-- .../grpc/sample/GrpcServerIntegrationTests.kt | 30 ++++++------ .../GrpcExceptionHandledServerCall.java | 46 +++++++++++++++++++ .../GrpcExceptionHandlerInterceptor.java | 8 ++-- 7 files changed, 85 insertions(+), 34 deletions(-) create mode 100644 spring-grpc-core/src/main/java/org/springframework/grpc/server/exception/GrpcExceptionHandledServerCall.java diff --git a/README.md b/README.md index e1e4c69c..6b500fef 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,8 @@ option java_outer_classname = "HelloWorldProto"; // The greeting service definition. service Simple { // Sends a greeting - rpc SayHello(HelloRequest) returns (HelloReply) {} + rpc SayHello (HelloRequest) returns (HelloReply) { + } rpc StreamHello(HelloRequest) returns (stream HelloReply) {} } diff --git a/samples/grpc-server-kotlin/pom.xml b/samples/grpc-server-kotlin/pom.xml index da74b489..0f8f81e5 100644 --- a/samples/grpc-server-kotlin/pom.xml +++ b/samples/grpc-server-kotlin/pom.xml @@ -209,17 +209,19 @@ compile-custom - - grpc-kotlin - - compile-custom - - - grpc-kotlin - - + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + false + + **/*.class + + + diff --git a/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcClientApplicationTests.kt b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcClientApplicationTests.kt index ab5671ef..3384bbe7 100644 --- a/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcClientApplicationTests.kt +++ b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcClientApplicationTests.kt @@ -19,7 +19,7 @@ import org.springframework.grpc.test.AutoConfigureInProcessTransport @SpringBootTest @AutoConfigureInProcessTransport -internal class NoAutowiredClients { +class NoAutowiredClients { @Autowired private lateinit var context: ApplicationContext @@ -36,7 +36,7 @@ internal class NoAutowiredClients { @SpringBootTest(properties = ["spring.grpc.client.default-channel.address=0.0.0.0:9090"]) @AutoConfigureInProcessTransport -internal class DefaultAutowiredClients { +class DefaultAutowiredClients { @Autowired private lateinit var context: ApplicationContext @@ -59,7 +59,7 @@ internal class DefaultAutowiredClients { properties = ["spring.grpc.client.default-channel.address=0.0.0.0:9090"] ) @AutoConfigureInProcessTransport -internal class SpecificAutowiredClients { +class SpecificAutowiredClients { @Autowired private lateinit var context: ApplicationContext diff --git a/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerHealthIntegrationTests.kt b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerHealthIntegrationTests.kt index d0be3aaf..d0bfaf16 100644 --- a/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerHealthIntegrationTests.kt +++ b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerHealthIntegrationTests.kt @@ -50,7 +50,7 @@ import java.time.Duration properties = ["spring.grpc.server.port=0", "spring.grpc.client.channels.health-test.address=static://0.0.0.0:\${local.grpc.port}", "spring.grpc.client.channels.health-test.health.enabled=true", "spring.grpc.client.channels.health-test.health.service-name=my-service"] ) @DirtiesContext -internal class WithClientHealthEnabled { +class WithClientHealthEnabled { @Test fun loadBalancerRespectsServerHealth( @Autowired channels: GrpcChannelFactory, @@ -112,7 +112,7 @@ internal class WithClientHealthEnabled { ) @AutoConfigureInProcessTransport @DirtiesContext -internal class WithActuatorHealthAdapter { +class WithActuatorHealthAdapter { @Test fun healthIndicatorsAdaptedToGrpcHealthStatus( @Autowired channels: GrpcChannelFactory, @@ -158,7 +158,7 @@ internal class WithActuatorHealthAdapter { } } - internal class CustomHealthIndicator : HealthIndicator { + class CustomHealthIndicator : HealthIndicator { override fun health(): Health? { return if (SERVICE_IS_UP) Health.up().build() else Health.down().build() } diff --git a/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerIntegrationTests.kt b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerIntegrationTests.kt index f8b18a92..232076f1 100644 --- a/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerIntegrationTests.kt +++ b/samples/grpc-server-kotlin/src/test/kotlin/org/springframework/grpc/sample/GrpcServerIntegrationTests.kt @@ -52,7 +52,7 @@ import java.util.concurrent.atomic.AtomicInteger @SpringBootTest @AutoConfigureInProcessTransport -internal class ServerWithInProcessChannel { +class ServerWithInProcessChannel { @Test fun servesResponseToClient(@Autowired channels: GrpcChannelFactory) { assertThatResponseIsServedToChannel(channels.createChannel("0.0.0.0:0")) @@ -62,7 +62,7 @@ internal class ServerWithInProcessChannel { @SpringBootTest @AutoConfigureInProcessTransport -internal class ServerWithException { +class ServerWithException { @Test fun specificErrorResponse(@Autowired channels: GrpcChannelFactory) { @@ -81,7 +81,7 @@ internal class ServerWithException { @Test fun defaultErrorResponseIsUnknown(@Autowired channels: GrpcChannelFactory) { val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")) - Assertions.assertThat( + Assertions.assertThat( Assert.assertThrows( StatusRuntimeException::class.java ) { client.sayHello(HelloRequest.newBuilder().setName("internal").build()) } @@ -94,14 +94,14 @@ internal class ServerWithException { @SpringBootTest @AutoConfigureInProcessTransport -internal class ServerWithExceptionInInterceptorCall { +class ServerWithExceptionInInterceptorCall { @Test fun specificErrorResponse(@Autowired channels: GrpcChannelFactory) { val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")) Assertions.assertThat( Assert.assertThrows( StatusRuntimeException::class.java - ) { client.sayHello(HelloRequest.newBuilder().setName("foo").build()) } + ) { client.sayHello(HelloRequest.newBuilder().setName("error").build()) } .status .code ).isEqualTo(Status.Code.INVALID_ARGUMENT) @@ -129,7 +129,7 @@ internal class ServerWithExceptionInInterceptorCall { @SpringBootTest @AutoConfigureInProcessTransport -internal class ServerWithExceptionInInterceptorListener { +class ServerWithExceptionInInterceptorListener { @Test fun specificErrorResponse( @Autowired channels: GrpcChannelFactory, @@ -150,7 +150,7 @@ internal class ServerWithExceptionInInterceptorListener { } @TestConfiguration - internal open class TestConfig { + open class TestConfig { companion object { var callCount: AtomicInteger = AtomicInteger() var messageCount: AtomicInteger = AtomicInteger() @@ -206,7 +206,7 @@ internal class ServerWithExceptionInInterceptorListener { @SpringBootTest("spring.grpc.server.exception-handler.enabled=false") @AutoConfigureInProcessTransport -internal class ServerWithUnhandledException { +class ServerWithUnhandledException { @Test fun specificErrorResponse(@Autowired channels: GrpcChannelFactory) { val client = SimpleGrpc.newBlockingStub(channels.createChannel("0.0.0.0:0")) @@ -236,7 +236,7 @@ internal class ServerWithUnhandledException { @SpringBootTest(properties = ["spring.grpc.server.host=0.0.0.0", "spring.grpc.server.port=0"]) -internal class ServerWithAnyIPv4AddressAndRandomPort { +class ServerWithAnyIPv4AddressAndRandomPort { @Test fun servesResponseToClientWithAnyIPv4AddressAndRandomPort( @Autowired channels: GrpcChannelFactory, @@ -248,7 +248,7 @@ internal class ServerWithAnyIPv4AddressAndRandomPort { @SpringBootTest(properties = ["spring.grpc.server.host=::", "spring.grpc.server.port=0"]) -internal class ServerWithAnyIPv6AddressAndRandomPort { +class ServerWithAnyIPv6AddressAndRandomPort { @Test fun servesResponseToClientWithAnyIPv4AddressAndRandomPort( @Autowired channels: GrpcChannelFactory, @@ -260,7 +260,7 @@ internal class ServerWithAnyIPv6AddressAndRandomPort { @SpringBootTest(properties = ["spring.grpc.server.host=127.0.0.1", "spring.grpc.server.port=0"]) -internal class ServerWithLocalhostAndRandomPort { +class ServerWithLocalhostAndRandomPort { @Test fun servesResponseToClientWithLocalhostAndRandomPort( @Autowired channels: GrpcChannelFactory, @@ -275,7 +275,7 @@ internal class ServerWithLocalhostAndRandomPort { properties = ["spring.grpc.server.port=0", "spring.grpc.client.channels.test-channel.address=static://0.0.0.0:\${local.grpc.port}"] ) @DirtiesContext -internal class ServerConfiguredWithStaticClientChannel { +class ServerConfiguredWithStaticClientChannel { @Test fun servesResponseToClientWithConfiguredChannel(@Autowired channels: GrpcChannelFactory) { assertThatResponseIsServedToChannel(channels.createChannel("test-channel")) @@ -285,7 +285,7 @@ internal class ServerConfiguredWithStaticClientChannel { @SpringBootTest(properties = ["spring.grpc.server.address=unix:unix-test-channel"]) @EnabledOnOs(OS.LINUX) -internal class ServerWithUnixDomain { +class ServerWithUnixDomain { @Test fun clientChannelWithUnixDomain(@Autowired channels: GrpcChannelFactory) { assertThatResponseIsServedToChannel( @@ -304,7 +304,7 @@ internal class ServerWithUnixDomain { ) @ActiveProfiles("ssl") @DirtiesContext -internal class ServerWithSsl { +class ServerWithSsl { @Test fun clientChannelWithSsl(@Autowired channels: GrpcChannelFactory) { assertThatResponseIsServedToChannel(channels.createChannel("test-channel")) @@ -317,7 +317,7 @@ internal class ServerWithSsl { ) @ActiveProfiles("ssl") @DirtiesContext -internal class ServerWithClientAuth { +class ServerWithClientAuth { @Test fun clientChannelWithSsl(@Autowired channels: GrpcChannelFactory) { assertThatResponseIsServedToChannel(channels.createChannel("test-channel")) diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/exception/GrpcExceptionHandledServerCall.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/exception/GrpcExceptionHandledServerCall.java new file mode 100644 index 00000000..d4d073dd --- /dev/null +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/exception/GrpcExceptionHandledServerCall.java @@ -0,0 +1,46 @@ +/* + * Copyright 2024-2024 the original author or authors. + * + * 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 org.springframework.grpc.server.exception; + +import io.grpc.ForwardingServerCall; +import io.grpc.Metadata; +import io.grpc.ServerCall; +import io.grpc.Status; +import io.grpc.StatusException; + +public class GrpcExceptionHandledServerCall + extends ForwardingServerCall.SimpleForwardingServerCall { + + private final GrpcExceptionHandler exceptionHandler; + + protected GrpcExceptionHandledServerCall(ServerCall delegate, GrpcExceptionHandler handler) { + super(delegate); + this.exceptionHandler = handler; + } + + @Override + public void close(Status status, Metadata trailers) { + if (status.getCode() == Status.Code.UNKNOWN && status.getCause() != null) { + final Throwable cause = status.getCause(); + final StatusException statusException = this.exceptionHandler.handleException(cause); + super.close(statusException.getStatus(), trailers); + } + else { + super.close(status, trailers); + } + } + +} diff --git a/spring-grpc-core/src/main/java/org/springframework/grpc/server/exception/GrpcExceptionHandlerInterceptor.java b/spring-grpc-core/src/main/java/org/springframework/grpc/server/exception/GrpcExceptionHandlerInterceptor.java index 79d34171..6279c3e0 100644 --- a/spring-grpc-core/src/main/java/org/springframework/grpc/server/exception/GrpcExceptionHandlerInterceptor.java +++ b/spring-grpc-core/src/main/java/org/springframework/grpc/server/exception/GrpcExceptionHandlerInterceptor.java @@ -65,16 +65,18 @@ public Listener interceptCall(ServerCall call, ServerCallHandler next) { Listener listener; FallbackHandler handler = new FallbackHandler(this.exceptionHandler); + final GrpcExceptionHandledServerCall exceptionHandledServerCall = new GrpcExceptionHandledServerCall<>( + call, handler); try { - listener = next.startCall(call, headers); + listener = next.startCall(exceptionHandledServerCall, headers); } catch (Throwable t) { - call.close(handler.handleException(t).getStatus(), headers(t)); + exceptionHandledServerCall.close(handler.handleException(t).getStatus(), headers(t)); listener = new Listener() { }; return listener; } - return new ExceptionHandlerListener<>(listener, call, handler); + return new ExceptionHandlerListener<>(listener, exceptionHandledServerCall, handler); } private static Metadata headers(Throwable t) {