Skip to content

Commit

Permalink
Add example for h2 keep alive (#2203)
Browse files Browse the repository at this point in the history
Motivation:
Add an example demonstrating how the HTTP/2 transport keep alive feature can be used for gRPC clients and servers.
  • Loading branch information
Scottmitch authored May 6, 2022
1 parent 9aec0e0 commit 7cd1c78
Show file tree
Hide file tree
Showing 11 changed files with 439 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
15 changes: 15 additions & 0 deletions servicetalk-examples/docs/modules/ROOT/pages/grpc/index.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 13 additions & 0 deletions servicetalk-examples/grpc/keepalive/README.adoc
Original file line number Diff line number Diff line change
@@ -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.
80 changes: 80 additions & 0 deletions servicetalk-examples/grpc/keepalive/build.gradle
Original file line number Diff line number Diff line change
@@ -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"
}
97 changes: 97 additions & 0 deletions servicetalk-examples/grpc/keepalive/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.servicetalk.examples.grpc.helloworld</groupId>
<artifactId>helloworld-maven</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>st-examples</name>
<url>https://servicetalk.io</url>

<properties>
<!-- servicetalk.version is updated automatically by release.sh during the ServiceTalk release process -->
<servicetalk.version>0.42.0</servicetalk.version>
<protobuf-maven-plugin.version>0.6.1</protobuf-maven-plugin.version>
<protoc.version>3.19.2</protoc.version>
<os-maven-plugin.version>1.6.0</os-maven-plugin.version>
<maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
</properties>

<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>${os-maven-plugin.version}</version>
</extension>
</extensions>

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>${protobuf-maven-plugin.version}</version>
<extensions>true</extensions>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protoc.version}:exe:${os.detected.classifier}</protocArtifact>
<protocPlugins>
<protocPlugin>
<id>servicetalk-grpc-protoc</id>
<groupId>io.servicetalk</groupId>
<artifactId>servicetalk-grpc-protoc</artifactId>
<version>${servicetalk.version}</version>
<mainClass>io.servicetalk.grpc.protoc.Main</mainClass>
</protocPlugin>
</protocPlugins>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.servicetalk</groupId>
<artifactId>servicetalk-bom</artifactId>
<version>${servicetalk.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>io.servicetalk</groupId>
<artifactId>servicetalk-annotations</artifactId>
</dependency>
<dependency>
<groupId>io.servicetalk</groupId>
<artifactId>servicetalk-grpc-netty</artifactId>
</dependency>
<dependency>
<groupId>io.servicetalk</groupId>
<artifactId>servicetalk-grpc-protoc</artifactId>
</dependency>
<dependency>
<groupId>io.servicetalk</groupId>
<artifactId>servicetalk-grpc-protobuf</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -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 <T> The type of items to iterate.
*/
private static final class StdInIterable<T> implements Iterable<T> {
private final Supplier<T> itemSupplier;

private StdInIterable(final Supplier<T> itemSupplier) {
this.itemSupplier = itemSupplier;
}

@Override
public Iterator<T> iterator() {
return new Iterator<T>() {
@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();
}
};
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Loading

0 comments on commit 7cd1c78

Please sign in to comment.