Skip to content

Commit ccd67ba

Browse files
authored
ensures connection is closed on keepalive timeout (#1118)
* ensures connection is close on keepalive timeout Signed-off-by: Oleh Dokuka <[email protected]> * fix format Signed-off-by: Oleh Dokuka <[email protected]> * improve KeepaliveTest Signed-off-by: Oleh Dokuka <[email protected]> * fix format and failing test Signed-off-by: Oleh Dokuka <[email protected]> * adds reference to the original GH issue Signed-off-by: Oleh Dokuka <[email protected]> * fixes google format Signed-off-by: Oleh Dokuka <[email protected]> --------- Signed-off-by: Oleh Dokuka <[email protected]>
1 parent 9bc30c4 commit ccd67ba

File tree

3 files changed

+191
-1
lines changed

3 files changed

+191
-1
lines changed

rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,7 @@ private void tryTerminateOnKeepAlive(KeepAliveSupport.KeepAlive keepAlive) {
312312
() ->
313313
new ConnectionErrorException(
314314
String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis())));
315+
getDuplexConnection().dispose();
315316
}
316317

317318
private void tryShutdown(Throwable e) {
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
package io.rsocket.integration;
2+
3+
import io.rsocket.Payload;
4+
import io.rsocket.RSocket;
5+
import io.rsocket.core.RSocketClient;
6+
import io.rsocket.core.RSocketConnector;
7+
import io.rsocket.core.RSocketServer;
8+
import io.rsocket.frame.decoder.PayloadDecoder;
9+
import io.rsocket.transport.netty.client.TcpClientTransport;
10+
import io.rsocket.transport.netty.server.CloseableChannel;
11+
import io.rsocket.transport.netty.server.TcpServerTransport;
12+
import io.rsocket.util.DefaultPayload;
13+
import java.time.Duration;
14+
import java.util.concurrent.atomic.AtomicBoolean;
15+
import java.util.function.Function;
16+
import org.junit.jupiter.api.AfterEach;
17+
import org.junit.jupiter.api.BeforeEach;
18+
import org.junit.jupiter.api.Test;
19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
import reactor.core.publisher.Flux;
22+
import reactor.core.publisher.Mono;
23+
import reactor.netty.tcp.TcpClient;
24+
import reactor.netty.tcp.TcpServer;
25+
import reactor.test.StepVerifier;
26+
import reactor.util.retry.Retry;
27+
import reactor.util.retry.RetryBackoffSpec;
28+
29+
/**
30+
* Test case that reproduces the following <a
31+
* href="https://github.com/rsocket/rsocket-java/issues/1099">GitHub Issue</a>
32+
*/
33+
public class KeepaliveTest {
34+
35+
private static final Logger LOG = LoggerFactory.getLogger(KeepaliveTest.class);
36+
private static final int PORT = 23200;
37+
38+
private CloseableChannel server;
39+
40+
@BeforeEach
41+
void setUp() {
42+
server = createServer().block();
43+
}
44+
45+
@AfterEach
46+
void tearDown() {
47+
server.dispose();
48+
server.onClose().block();
49+
}
50+
51+
@Test
52+
void keepAliveTest() {
53+
RSocketClient rsocketClient = createClient();
54+
55+
int expectedCount = 4;
56+
AtomicBoolean sleepOnce = new AtomicBoolean(true);
57+
StepVerifier.create(
58+
Flux.range(0, expectedCount)
59+
.delayElements(Duration.ofMillis(2000))
60+
.concatMap(
61+
i ->
62+
rsocketClient
63+
.requestResponse(Mono.just(DefaultPayload.create("")))
64+
.doOnNext(
65+
__ -> {
66+
if (sleepOnce.getAndSet(false)) {
67+
try {
68+
LOG.info("Sleeping...");
69+
Thread.sleep(1_000);
70+
LOG.info("Waking up.");
71+
} catch (InterruptedException e) {
72+
throw new RuntimeException(e);
73+
}
74+
}
75+
})
76+
.log("id " + i)
77+
.onErrorComplete()))
78+
.expectSubscription()
79+
.expectNextCount(expectedCount)
80+
.verifyComplete();
81+
}
82+
83+
@Test
84+
void keepAliveTestLazy() {
85+
Mono<RSocket> rsocketMono = createClientLazy();
86+
87+
int expectedCount = 4;
88+
AtomicBoolean sleepOnce = new AtomicBoolean(true);
89+
StepVerifier.create(
90+
Flux.range(0, expectedCount)
91+
.delayElements(Duration.ofMillis(2000))
92+
.concatMap(
93+
i ->
94+
rsocketMono.flatMap(
95+
rsocket ->
96+
rsocket
97+
.requestResponse(DefaultPayload.create(""))
98+
.doOnNext(
99+
__ -> {
100+
if (sleepOnce.getAndSet(false)) {
101+
try {
102+
LOG.info("Sleeping...");
103+
Thread.sleep(1_000);
104+
LOG.info("Waking up.");
105+
} catch (InterruptedException e) {
106+
throw new RuntimeException(e);
107+
}
108+
}
109+
})
110+
.log("id " + i)
111+
.onErrorComplete())))
112+
.expectSubscription()
113+
.expectNextCount(expectedCount)
114+
.verifyComplete();
115+
}
116+
117+
private static Mono<CloseableChannel> createServer() {
118+
LOG.info("Starting server at port {}", PORT);
119+
120+
TcpServer tcpServer = TcpServer.create().host("localhost").port(PORT);
121+
122+
return RSocketServer.create(
123+
(setupPayload, rSocket) -> {
124+
rSocket
125+
.onClose()
126+
.doFirst(() -> LOG.info("Connected on server side."))
127+
.doOnTerminate(() -> LOG.info("Connection closed on server side."))
128+
.subscribe();
129+
130+
return Mono.just(new MyServerRsocket());
131+
})
132+
.payloadDecoder(PayloadDecoder.ZERO_COPY)
133+
.bind(TcpServerTransport.create(tcpServer))
134+
.doOnNext(closeableChannel -> LOG.info("RSocket server started."));
135+
}
136+
137+
private static RSocketClient createClient() {
138+
LOG.info("Connecting....");
139+
140+
Function<String, RetryBackoffSpec> reconnectSpec =
141+
reason ->
142+
Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(10L))
143+
.doBeforeRetry(retrySignal -> LOG.info("Reconnecting. Reason: {}", reason));
144+
145+
Mono<RSocket> rsocketMono =
146+
RSocketConnector.create()
147+
.fragment(16384)
148+
.reconnect(reconnectSpec.apply("connector-close"))
149+
.keepAlive(Duration.ofMillis(100L), Duration.ofMillis(900L))
150+
.connect(TcpClientTransport.create(TcpClient.create().host("localhost").port(PORT)));
151+
152+
RSocketClient client = RSocketClient.from(rsocketMono);
153+
154+
client
155+
.source()
156+
.doOnNext(r -> LOG.info("Got RSocket"))
157+
.flatMap(RSocket::onClose)
158+
.doOnError(err -> LOG.error("Error during onClose.", err))
159+
.retryWhen(reconnectSpec.apply("client-close"))
160+
.doFirst(() -> LOG.info("Connected on client side."))
161+
.doOnTerminate(() -> LOG.info("Connection closed on client side."))
162+
.repeat()
163+
.subscribe();
164+
165+
return client;
166+
}
167+
168+
private static Mono<RSocket> createClientLazy() {
169+
LOG.info("Connecting....");
170+
171+
Function<String, RetryBackoffSpec> reconnectSpec =
172+
reason ->
173+
Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(10L))
174+
.doBeforeRetry(retrySignal -> LOG.info("Reconnecting. Reason: {}", reason));
175+
176+
return RSocketConnector.create()
177+
.fragment(16384)
178+
.reconnect(reconnectSpec.apply("connector-close"))
179+
.keepAlive(Duration.ofMillis(100L), Duration.ofMillis(900L))
180+
.connect(TcpClientTransport.create(TcpClient.create().host("localhost").port(PORT)));
181+
}
182+
183+
public static class MyServerRsocket implements RSocket {
184+
185+
@Override
186+
public Mono<Payload> requestResponse(Payload payload) {
187+
return Mono.just("Pong").map(DefaultPayload::create);
188+
}
189+
}
190+
}

rsocket-transport-netty/src/test/resources/logback-test.xml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
<logger name="io.rsocket.FrameLogger" level="INFO"/>
2828
<logger name="io.rsocket.fragmentation.FragmentationDuplexConnection" level="INFO"/>
2929
<logger name="io.rsocket.transport.netty" level="INFO"/>
30-
<logger name="io.rsocket.FrameLogger" level="INFO"/>
3130
<logger name="io.rsocket.core.RSocketRequester" level="DEBUG"/>
3231
<logger name="io.rsocket.core.RSocketResponder" level="DEBUG"/>
3332
<logger name="io.rsocket.test.TransportTest" level="DEBUG"/>

0 commit comments

Comments
 (0)