Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add example for h2 keep alive #2203

Merged
merged 6 commits into from
May 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Scottmitch marked this conversation as resolved.
Show resolved Hide resolved
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.
Scottmitch marked this conversation as resolved.
Show resolved Hide resolved
// Using the default value is suitable in most scenarios, but if you want to customize the value
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The improved comments will really help.

h2().keepAlivePolicy(new KeepAlivePolicyBuilder().build())

is needed to configure the default. Perhaps a constant defaultKeepAlive() or useDefaults() for configuring the default.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we expose the builder type I don't think the static factory helper methods provide much value and add duplication.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

True, but I had to spend a couple minutes figuring out that there was no default object and how to make one (the Builder docs don't mention that default values will be use if not supplied). Oddly, we provide a constant for disabling keep alive which is the default behaviour (not clear why you would ever needed it except to be explicit). Some mechanism or doc suggestion assistance of how to get a default keep alive policy would help IMO.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets consider this for a followup PR to make any changes more explicit. I would prefer to remove the disabled() API and just use null if we need a default (one less API, no special "by reference" comparison required).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All are fine approaches, whatever ensures users don't have to read the source to figure out how to get the default.

// 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