Skip to content

Commit 29c4f7d

Browse files
author
Nitesh Kant
authored
Introduce HTTP/2 keep alive (#1029)
__Motivation__ HTTP/2 PING frames are useful to keep a connection alive relatively cheaply in presence of long running streams. ServiceTalk should support adding this functionality for both clients and servers. __Modification__ - Add `KeepAlivePolicy` to `H2ProtocolConfig` that can be configured to enable keep-alive behavior on either clients or servers. Following features are provided: -- Specify an idleness threshold, after which a PING frame will be sent on the connection to detect liveness. -- Specify an ack timeout, within which we expect an ack for the sent PING. If no ack is received, the connection is closed. -- Specify whether PING frames should be sent even when there are no active streams. This defaults to `false`. __Result__ Keep alive behavior can be enabled for either client or server.
1 parent b22e5ea commit 29c4f7d

File tree

12 files changed

+1133
-183
lines changed

12 files changed

+1133
-183
lines changed
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
/*
2+
* Copyright © 2020 Apple Inc. and the ServiceTalk project authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.servicetalk.grpc.netty;
17+
18+
import io.servicetalk.concurrent.api.CompositeCloseable;
19+
import io.servicetalk.concurrent.api.Publisher;
20+
import io.servicetalk.concurrent.api.Single;
21+
import io.servicetalk.concurrent.internal.ServiceTalkTestTimeout;
22+
import io.servicetalk.grpc.api.GrpcClientBuilder;
23+
import io.servicetalk.grpc.api.GrpcServerBuilder;
24+
import io.servicetalk.grpc.api.GrpcServiceContext;
25+
import io.servicetalk.grpc.netty.TesterProto.TestRequest;
26+
import io.servicetalk.grpc.netty.TesterProto.TestResponse;
27+
import io.servicetalk.grpc.netty.TesterProto.Tester.ServiceFactory;
28+
import io.servicetalk.grpc.netty.TesterProto.Tester.TesterClient;
29+
import io.servicetalk.grpc.netty.TesterProto.Tester.TesterService;
30+
import io.servicetalk.http.netty.H2KeepAlivePolicies;
31+
import io.servicetalk.http.netty.H2ProtocolConfig;
32+
import io.servicetalk.http.netty.HttpProtocolConfigs;
33+
import io.servicetalk.transport.api.HostAndPort;
34+
import io.servicetalk.transport.api.ServerContext;
35+
import io.servicetalk.transport.api.ServiceTalkSocketOptions;
36+
37+
import org.junit.After;
38+
import org.junit.Rule;
39+
import org.junit.Test;
40+
import org.junit.rules.Timeout;
41+
import org.junit.runner.RunWith;
42+
import org.junit.runners.Parameterized;
43+
44+
import java.net.InetSocketAddress;
45+
import java.time.Duration;
46+
import java.util.Collection;
47+
import java.util.concurrent.TimeoutException;
48+
import java.util.function.Function;
49+
50+
import static io.servicetalk.concurrent.api.AsyncCloseables.newCompositeCloseable;
51+
import static io.servicetalk.concurrent.api.Publisher.never;
52+
import static io.servicetalk.concurrent.api.Single.succeeded;
53+
import static io.servicetalk.transport.netty.internal.AddressUtils.localAddress;
54+
import static io.servicetalk.transport.netty.internal.AddressUtils.serverHostAndPort;
55+
import static java.time.Duration.ofSeconds;
56+
import static java.util.Arrays.asList;
57+
import static java.util.concurrent.TimeUnit.MILLISECONDS;
58+
import static java.util.concurrent.TimeUnit.MINUTES;
59+
import static org.hamcrest.Matchers.is;
60+
import static org.junit.Assert.fail;
61+
import static org.junit.Assume.assumeThat;
62+
63+
@RunWith(Parameterized.class)
64+
public class KeepAliveTest {
65+
private final TesterClient client;
66+
private final ServerContext ctx;
67+
68+
@Rule
69+
public final Timeout timeout = new ServiceTalkTestTimeout(1, MINUTES);
70+
private final long idleTimeoutMillis;
71+
72+
public KeepAliveTest(final boolean keepAlivesFromClient,
73+
final Function<String, H2ProtocolConfig> protocolConfigSupplier,
74+
final long idleTimeoutMillis) throws Exception {
75+
this.idleTimeoutMillis = idleTimeoutMillis;
76+
GrpcServerBuilder serverBuilder = GrpcServers.forAddress(localAddress(0));
77+
if (!keepAlivesFromClient) {
78+
serverBuilder.protocols(protocolConfigSupplier.apply("servicetalk-tests-server-wire-logger"));
79+
} else {
80+
serverBuilder.socketOption(ServiceTalkSocketOptions.IDLE_TIMEOUT, idleTimeoutMillis)
81+
.protocols(HttpProtocolConfigs.h2()
82+
.enableFrameLogging("servicetalk-tests-server-wire-logger").build());
83+
}
84+
ctx = serverBuilder.listenAndAwait(new ServiceFactory(new InfiniteStreamsService()));
85+
GrpcClientBuilder<HostAndPort, InetSocketAddress> clientBuilder =
86+
GrpcClients.forAddress(serverHostAndPort(ctx));
87+
if (keepAlivesFromClient) {
88+
clientBuilder.protocols(protocolConfigSupplier.apply("servicetalk-tests-client-wire-logger"));
89+
} else {
90+
clientBuilder.socketOption(ServiceTalkSocketOptions.IDLE_TIMEOUT, idleTimeoutMillis)
91+
.protocols(HttpProtocolConfigs.h2()
92+
.enableFrameLogging("servicetalk-tests-client-wire-logger").build());
93+
}
94+
client = clientBuilder.build(new TesterProto.Tester.ClientFactory());
95+
}
96+
97+
@Parameterized.Parameters(name = "keepAlivesFromClient? {0}, idleTimeout: {2}")
98+
public static Collection<Object[]> data() {
99+
return asList(newParam(true, ofSeconds(10), ofSeconds(12)),
100+
newParam(false, ofSeconds(10), ofSeconds(12)));
101+
}
102+
103+
private static Object[] newParam(final boolean keepAlivesFromClient, final Duration keepAliveIdleDuration,
104+
final Duration idleTimeoutDuration) {
105+
return new Object[] {keepAlivesFromClient,
106+
(Function<String, H2ProtocolConfig>) frameLogger ->
107+
HttpProtocolConfigs.h2()
108+
.keepAlivePolicy(H2KeepAlivePolicies.whenIdleFor(keepAliveIdleDuration))
109+
.enableFrameLogging(frameLogger).build(),
110+
idleTimeoutDuration.toMillis()};
111+
}
112+
113+
@After
114+
public void tearDown() throws Exception {
115+
CompositeCloseable closeable = newCompositeCloseable().appendAll(client, ctx);
116+
closeable.close();
117+
}
118+
119+
@Test
120+
public void bidiStream() throws Exception {
121+
// Ignore test on CI due to high timeouts
122+
assumeThat(ServiceTalkTestTimeout.CI, is(false));
123+
124+
try {
125+
client.testBiDiStream(never()).toFuture().get(idleTimeoutMillis + 100, MILLISECONDS);
126+
fail("Unexpected response available.");
127+
} catch (TimeoutException e) {
128+
// expected
129+
}
130+
}
131+
132+
@Test
133+
public void requestStream() throws Exception {
134+
// Ignore test on CI due to high timeouts
135+
assumeThat(ServiceTalkTestTimeout.CI, is(false));
136+
137+
try {
138+
client.testRequestStream(never()).toFuture().get(idleTimeoutMillis + 100, MILLISECONDS);
139+
fail("Unexpected response available.");
140+
} catch (TimeoutException e) {
141+
// expected
142+
}
143+
}
144+
145+
@Test
146+
public void responseStream() throws Exception {
147+
// Ignore test on CI due to high timeouts
148+
assumeThat(ServiceTalkTestTimeout.CI, is(false));
149+
150+
try {
151+
client.testResponseStream(TestRequest.newBuilder().build())
152+
.toFuture().get(idleTimeoutMillis + 100, MILLISECONDS);
153+
fail("Unexpected response available.");
154+
} catch (TimeoutException e) {
155+
// expected
156+
}
157+
}
158+
159+
private static final class InfiniteStreamsService implements TesterService {
160+
161+
@Override
162+
public Publisher<TestResponse> testBiDiStream(final GrpcServiceContext ctx,
163+
final Publisher<TestRequest> request) {
164+
return request.map(testRequest -> TestResponse.newBuilder().build());
165+
}
166+
167+
@Override
168+
public Single<TestResponse> testRequestStream(final GrpcServiceContext ctx,
169+
final Publisher<TestRequest> request) {
170+
return request.collect(() -> null, (testResponse, testRequest) -> null)
171+
.map(__ -> TestResponse.newBuilder().build());
172+
}
173+
174+
@Override
175+
public Publisher<TestResponse> testResponseStream(final GrpcServiceContext ctx, final TestRequest request) {
176+
return never();
177+
}
178+
179+
@Override
180+
public Single<TestResponse> test(final GrpcServiceContext ctx, final TestRequest request) {
181+
return succeeded(TestResponse.newBuilder().build());
182+
}
183+
}
184+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright © 2020 Apple Inc. and the ServiceTalk project authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.servicetalk.http.netty;
17+
18+
import io.servicetalk.http.netty.H2ProtocolConfig.KeepAlivePolicy;
19+
20+
import java.time.Duration;
21+
22+
import static java.util.Objects.requireNonNull;
23+
24+
final class DefaultKeepAlivePolicy implements KeepAlivePolicy {
25+
private final Duration idleDuration;
26+
private final Duration ackTimeout;
27+
private final boolean withoutActiveStreams;
28+
29+
DefaultKeepAlivePolicy(final Duration idleDuration, final Duration ackTimeout, final boolean withoutActiveStreams) {
30+
this.idleDuration = requireNonNull(idleDuration);
31+
this.ackTimeout = requireNonNull(ackTimeout);
32+
this.withoutActiveStreams = withoutActiveStreams;
33+
}
34+
35+
@Override
36+
public Duration idleDuration() {
37+
return idleDuration;
38+
}
39+
40+
@Override
41+
public Duration ackTimeout() {
42+
return ackTimeout;
43+
}
44+
45+
@Override
46+
public boolean withoutActiveStreams() {
47+
return withoutActiveStreams;
48+
}
49+
}

servicetalk-http-netty/src/main/java/io/servicetalk/http/netty/H2ClientParentConnectionContext.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@
8181
final class H2ClientParentConnectionContext extends H2ParentConnectionContext {
8282
private H2ClientParentConnectionContext(Channel channel, BufferAllocator allocator, Executor executor,
8383
FlushStrategy flushStrategy, @Nullable Long idleTimeoutMs,
84-
HttpExecutionStrategy executionStrategy) {
85-
super(channel, allocator, executor, flushStrategy, idleTimeoutMs, executionStrategy);
84+
HttpExecutionStrategy executionStrategy,
85+
final KeepAliveManager keepAliveManager) {
86+
super(channel, allocator, executor, flushStrategy, idleTimeoutMs, executionStrategy, keepAliveManager);
8687
}
8788

8889
interface H2ClientParentConnection extends FilterableStreamingHttpConnection, NettyConnectionContext {
@@ -103,8 +104,10 @@ protected void handleSubscribe(final Subscriber<? super H2ClientParentConnection
103104
final ChannelPipeline pipeline;
104105
try {
105106
delayedCancellable = new DelayedCancellable();
107+
KeepAliveManager keepAliveManager = new KeepAliveManager(channel, config.keepAlivePolicy());
106108
H2ClientParentConnectionContext connection = new H2ClientParentConnectionContext(channel,
107-
allocator, executor, parentFlushStrategy, idleTimeoutMs, executionStrategy);
109+
allocator, executor, parentFlushStrategy, idleTimeoutMs, executionStrategy,
110+
keepAliveManager);
108111
channel.attr(CHANNEL_CLOSEABLE_KEY).set(connection);
109112
// We need the NettyToStChannelInboundHandler to be last in the pipeline. We accomplish that by
110113
// calling the ChannelInitializer before we do addLast for the NettyToStChannelInboundHandler.

0 commit comments

Comments
 (0)