diff --git a/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc b/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc index 1994cbcb2f..7f5140971e 100644 --- a/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc +++ b/servicetalk-examples/docs/modules/ROOT/pages/_partials/nav-versioned.adoc @@ -20,6 +20,7 @@ ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#HelloWorld[Hello World] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#Compression[Compression] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#Deadlines[Deadlines] +** xref:{page-version}@servicetalk-examples::grpc/index.adoc#KeepAlive[Keep Alive] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#Observer[Observer] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#Health[Health Checking] ** xref:{page-version}@servicetalk-examples::grpc/index.adoc#errors[Application Errors] diff --git a/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc b/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc index 961770886c..6e4646b597 100644 --- a/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc +++ b/servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc @@ -96,6 +96,21 @@ https://grpc.io/docs/what-is-grpc/core-concepts/#deadlines[gRPC deadlines] (aka – Sends hello requests to the server with 1 minute deadline and 3 second deadline and receives a greeting response within that time or cancels the request. +[#KeepAlive] +== Keep Alive + +Demonstrates how to use HTTP/2 keep alive for gRPC +link:{source-root}/servicetalk-examples/grpc/keepalive/src/main/java/io/servicetalk/examples/grpc/keepalive/KeepAliveServer.java[server] +and +link:{source-root}/servicetalk-examples/grpc/keepalive/src/main/java/io/servicetalk/examples/grpc/keepalive/KeepAliveClient.java[client]. +Keep alive uses transport control frames to ensure the peer is still able to read and write to open connections. If the +peer is not able to respond to the control frame within the configured amount of time, the connection is closed. This +is useful if your environment doesn't provide other forms of connection keep alive (e.g. +link:https://docs.oracle.com/javase/8/docs/api/java/net/StandardSocketOptions.html#SO_KEEPALIVE[SO_KEEPALIVE], and maybe +preferred to lower level keep alive because it is closer the application logic (more likely if this check works, that +your application is able to read/write). Keep alive can be helpful to detect scenarios such as non-graceful disconnects +(e.g. power outage, ethernet cable pulled, buggy middle box) and general network disconnects. + [#Debugging] == Debugging diff --git a/servicetalk-examples/grpc/keepalive/README.adoc b/servicetalk-examples/grpc/keepalive/README.adoc new file mode 100644 index 0000000000..65ca5d88ec --- /dev/null +++ b/servicetalk-examples/grpc/keepalive/README.adoc @@ -0,0 +1,13 @@ +== ServiceTalk gRPC KeepAlive Example + +Demonstrates how to use HTTP/2 keep alive for gRPC +link:{source-root}/servicetalk-examples/grpc/keepalive/src/main/java/io/servicetalk/examples/grpc/keepalive/KeepAliveServer.java[server] +and +link:{source-root}/servicetalk-examples/grpc/keepalive/src/main/java/io/servicetalk/examples/grpc/keepalive/KeepAliveClient.java[client]. +Keep alive uses transport control frames to ensure the peer is still able to read and write to open connections. If the +peer is not able to respond to the control frame within the configured amount of time, the connection is closed. This +is useful if your environment doesn't provide other forms of connection keep alive (e.g. +link:https://docs.oracle.com/javase/8/docs/api/java/net/StandardSocketOptions.html#SO_KEEPALIVE[SO_KEEPALIVE], and maybe +preferred to lower level keep alive because it is closer the application logic (more likely if this check works, that +your application is able to read/write). Keep alive can be helpful to detect scenarios such as non-graceful disconnects +(e.g. power outage, ethernet cable pulled, buggy middle box) and general network disconnects. \ No newline at end of file diff --git a/servicetalk-examples/grpc/keepalive/build.gradle b/servicetalk-examples/grpc/keepalive/build.gradle new file mode 100644 index 0000000000..23e050aac0 --- /dev/null +++ b/servicetalk-examples/grpc/keepalive/build.gradle @@ -0,0 +1,80 @@ +/* + * Copyright © 2019 Apple Inc. and the ServiceTalk project 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +buildscript { + dependencies { + classpath "com.google.protobuf:protobuf-gradle-plugin:$protobufGradlePluginVersion" + } +} + +apply plugin: "java" +apply plugin: "com.google.protobuf" +apply from: "../../gradle/idea.gradle" + +dependencies { + implementation project(":servicetalk-annotations") + implementation project(":servicetalk-grpc-netty") + implementation project(":servicetalk-grpc-protoc") + implementation project(":servicetalk-grpc-protobuf") + + implementation "org.slf4j:slf4j-api:$slf4jVersion" + runtimeOnly "org.apache.logging.log4j:log4j-slf4j-impl:$log4jVersion" +} + +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:$protobufVersion" + } + + //// REMOVE if outside of ServiceTalk gradle project + def pluginJar = file("${project.rootProject.rootDir}/servicetalk-grpc-protoc/build" + + "/buildExecutable/servicetalk-grpc-protoc-${project.version}-all.jar") + //// REMOVE if outside of ServiceTalk gradle project + + plugins { + servicetalk_grpc { + //// REMOVE if outside of ServiceTalk gradle project - use "artifact" as demonstrated below + //// "path" is used only because we want to use the gradle project local version of the plugin. + path = pluginJar.path + //// REMOVE if outside of ServiceTalk gradle project - use "artifact" as demonstrated below + + // artifact = "io.servicetalk:servicetalk-grpc-protoc:$serviceTalkVersion:all@jar" + } + } + generateProtoTasks { + all().each { task -> + //// REMOVE if outside of ServiceTalk gradle project + task.dependsOn(":servicetalk-grpc-protoc:buildExecutable") // use gradle project local grpc-protoc dependency + + // you may need to manually add the artifact name as an input + task.inputs + .file(pluginJar) + .withNormalizer(ClasspathNormalizer) + .withPropertyName("servicetalkPluginJar") + .withPathSensitivity(PathSensitivity.RELATIVE) + //// REMOVE if outside of ServiceTalk gradle project + + task.plugins { + servicetalk_grpc { + // Need to tell protobuf-gradle-plugin to output in the correct directory if all generated + // code for a single proto goes to a single file (e.g. "java_multiple_files = false" in the .proto). + outputSubDir = "java" + } + } + } + } + generatedFilesBaseDir = "$buildDir/generated/sources/proto" +} diff --git a/servicetalk-examples/grpc/keepalive/pom.xml b/servicetalk-examples/grpc/keepalive/pom.xml new file mode 100644 index 0000000000..9406c0df65 --- /dev/null +++ b/servicetalk-examples/grpc/keepalive/pom.xml @@ -0,0 +1,97 @@ + + 4.0.0 + io.servicetalk.examples.grpc.helloworld + helloworld-maven + jar + 1.0-SNAPSHOT + st-examples + https://servicetalk.io + + + + 0.42.0 + 0.6.1 + 3.19.2 + 1.6.0 + 3.8.1 + + + + + + kr.motd.maven + os-maven-plugin + ${os-maven-plugin.version} + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 1.8 + 1.8 + + + + org.xolstice.maven.plugins + protobuf-maven-plugin + ${protobuf-maven-plugin.version} + true + + + + compile + + + com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier} + + + servicetalk-grpc-protoc + io.servicetalk + servicetalk-grpc-protoc + ${servicetalk.version} + io.servicetalk.grpc.protoc.Main + + + + + + + + + + + + + io.servicetalk + servicetalk-bom + ${servicetalk.version} + pom + import + + + + + + + io.servicetalk + servicetalk-annotations + + + io.servicetalk + servicetalk-grpc-netty + + + io.servicetalk + servicetalk-grpc-protoc + + + io.servicetalk + servicetalk-grpc-protobuf + + + diff --git a/servicetalk-examples/grpc/keepalive/src/main/java/io/servicetalk/examples/grpc/keepalive/KeepAliveClient.java b/servicetalk-examples/grpc/keepalive/src/main/java/io/servicetalk/examples/grpc/keepalive/KeepAliveClient.java new file mode 100644 index 0000000000..44502bb3cb --- /dev/null +++ b/servicetalk-examples/grpc/keepalive/src/main/java/io/servicetalk/examples/grpc/keepalive/KeepAliveClient.java @@ -0,0 +1,91 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.servicetalk.examples.grpc.keepalive; + +import io.servicetalk.grpc.netty.GrpcClients; + +import io.grpc.examples.keepalive.HelloReply; +import io.grpc.examples.keepalive.HelloRequest; +import io.grpc.examples.keepalive.StreamingGreeter.BlockingStreamingGreeterClient; +import io.grpc.examples.keepalive.StreamingGreeter.ClientFactory; + +import java.io.IOException; +import java.util.Iterator; +import java.util.function.Supplier; + +import static io.servicetalk.http.netty.H2KeepAlivePolicies.whenIdleFor; +import static io.servicetalk.http.netty.HttpProtocolConfigs.h2; +import static io.servicetalk.logging.api.LogLevel.TRACE; +import static java.time.Duration.ofSeconds; + +/** + * Example that demonstrates how to enable HTTP/2 keep alive for a gRPC client. + */ +public final class KeepAliveClient { + public static void main(String... args) throws Exception { + try (BlockingStreamingGreeterClient client = GrpcClients.forAddress("localhost", 8080) + .initializeHttp(httpBuilder -> httpBuilder.protocols( + // 4 second timeout is typically much shorter than necessary, but demonstrates PING frame traffic. + // Using the default value is suitable in most scenarios, but if you want to customize the value + // consider how many resources (network traffic, CPU for local timer management) vs time to detect + // bad connection. + // The keep alive is only sent when no traffic is detected, so if both peers have keep alive the + // faster interval will be the primary sender. + h2().keepAlivePolicy(whenIdleFor(ofSeconds(4))) + // Enable frame logging so we can see the PING frames sent/received. + .enableFrameLogging("servicetalk-examples-h2-frame-logger", TRACE, () -> true) + .build())) + .buildBlocking(new ClientFactory())) { + HelloReply reply = client.streamHello(new StdInIterable<>(() -> + HelloRequest.newBuilder().setName("World").build())); + System.out.println("Got reply: " + reply); + } + } + + /** + * Infinite input stream, each item is sent when data is read from std in. + * @param The type of items to iterate. + */ + private static final class StdInIterable implements Iterable { + private final Supplier itemSupplier; + + private StdInIterable(final Supplier itemSupplier) { + this.itemSupplier = itemSupplier; + } + + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + System.out.println("Press any key to send next item..."); + try { + @SuppressWarnings("unused") + int r = System.in.read(); + } catch (IOException e) { + throw new RuntimeException("Unexpected exception waiting for input", e); + } + return true; + } + + @Override + public T next() { + return itemSupplier.get(); + } + }; + } + } +} diff --git a/servicetalk-examples/grpc/keepalive/src/main/java/io/servicetalk/examples/grpc/keepalive/KeepAliveServer.java b/servicetalk-examples/grpc/keepalive/src/main/java/io/servicetalk/examples/grpc/keepalive/KeepAliveServer.java new file mode 100644 index 0000000000..a11b31edd3 --- /dev/null +++ b/servicetalk-examples/grpc/keepalive/src/main/java/io/servicetalk/examples/grpc/keepalive/KeepAliveServer.java @@ -0,0 +1,51 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.servicetalk.examples.grpc.keepalive; + +import io.servicetalk.concurrent.api.Single; +import io.servicetalk.grpc.netty.GrpcServers; + +import io.grpc.examples.keepalive.StreamingGreeter.StreamingGreeterService; + +import static io.servicetalk.http.netty.H2KeepAlivePolicies.whenIdleFor; +import static io.servicetalk.http.netty.HttpProtocolConfigs.h2; +import static io.servicetalk.logging.api.LogLevel.TRACE; +import static java.time.Duration.ofSeconds; + +/** + * Example that demonstrates how to enable HTTP/2 keep alive for a gRPC server. + */ +public final class KeepAliveServer { + public static void main(String... args) throws Exception { + GrpcServers.forPort(8080) + .initializeHttp(httpBuilder -> httpBuilder.protocols( + // 6 second timeout is typically much shorter than necessary, but demonstrates PING frame traffic. + // Using the default value is suitable in most scenarios, but if you want to customize the value + // consider how many resources (network traffic, CPU for local timer management) vs time to detect + // bad connection. + // The keep alive is only sent when no traffic is detected, so if both peers have keep alive the + // faster interval will be the primary sender. + h2().keepAlivePolicy(whenIdleFor(ofSeconds(6))) + // Enable frame logging so we can see the PING frames sent/received. + .enableFrameLogging("servicetalk-examples-h2-frame-logger", TRACE, () -> true) + .build())) + .listenAndAwait((StreamingGreeterService) (ctx, request) -> + request.whenOnNext(item -> System.out.println("Got request: " + item)).ignoreElements() + // Never return a response so we can see keep alive in action. + .concat(Single.never())) + .awaitShutdown(); + } +} diff --git a/servicetalk-examples/grpc/keepalive/src/main/java/io/servicetalk/examples/grpc/keepalive/package-info.java b/servicetalk-examples/grpc/keepalive/src/main/java/io/servicetalk/examples/grpc/keepalive/package-info.java new file mode 100644 index 0000000000..4f1b1f8573 --- /dev/null +++ b/servicetalk-examples/grpc/keepalive/src/main/java/io/servicetalk/examples/grpc/keepalive/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright © 2022 Apple Inc. and the ServiceTalk project 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@ElementsAreNonnullByDefault +package io.servicetalk.examples.grpc.keepalive; + +import io.servicetalk.annotations.ElementsAreNonnullByDefault; diff --git a/servicetalk-examples/grpc/keepalive/src/main/proto/helloworld.proto b/servicetalk-examples/grpc/keepalive/src/main/proto/helloworld.proto new file mode 100644 index 0000000000..b5719e50bd --- /dev/null +++ b/servicetalk-examples/grpc/keepalive/src/main/proto/helloworld.proto @@ -0,0 +1,32 @@ +// Copyright 2015 The gRPC 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 +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +syntax = "proto3"; + +option java_multiple_files = true; +option java_package = "io.grpc.examples.keepalive"; +option java_outer_classname = "KeepAliveProto"; + +package keepalive; + +service StreamingGreeter { + rpc StreamHello (stream HelloRequest) returns (HelloReply) {} +} + +message HelloRequest { + string name = 1; +} + +message HelloReply { + string message = 1; +} diff --git a/servicetalk-examples/grpc/keepalive/src/main/resources/log4j2.xml b/servicetalk-examples/grpc/keepalive/src/main/resources/log4j2.xml new file mode 100644 index 0000000000..f2b28781b3 --- /dev/null +++ b/servicetalk-examples/grpc/keepalive/src/main/resources/log4j2.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/settings.gradle b/settings.gradle index c142ddc673..1952486853 100644 --- a/settings.gradle +++ b/settings.gradle @@ -50,6 +50,7 @@ include "servicetalk-annotations", "servicetalk-examples:grpc:execution-strategy", "servicetalk-examples:grpc:observer", "servicetalk-examples:grpc:health", + "servicetalk-examples:grpc:keepalive", "servicetalk-examples:http:helloworld", "servicetalk-examples:http:mutual-tls", "servicetalk-examples:http:redirects", @@ -117,6 +118,7 @@ project(":servicetalk-examples:grpc:errors").name = "servicetalk-examples-grpc-e project(":servicetalk-examples:grpc:execution-strategy").name = "servicetalk-examples-grpc-execution-strategy" project(":servicetalk-examples:grpc:observer").name = "servicetalk-examples-grpc-observer" project(":servicetalk-examples:grpc:health").name = "servicetalk-examples-grpc-health" +project(":servicetalk-examples:grpc:keepalive").name = "servicetalk-examples-grpc-keepalive" project(":servicetalk-examples:http:http2").name = "servicetalk-examples-http-http2" project(":servicetalk-examples:http:helloworld").name = "servicetalk-examples-http-helloworld" project(":servicetalk-examples:http:debugging").name = "servicetalk-examples-http-debugging"