From 2c521ad5e72e6188f8d9f5f77ae8bf7a8019e8a9 Mon Sep 17 00:00:00 2001 From: "jeroen.veltman" Date: Tue, 31 Mar 2026 12:19:57 +0200 Subject: [PATCH 01/11] Add QUIC and HTTP/3 netty transport prototypes Signed-off-by: jeroen.veltman --- rsocket-transport-netty/build.gradle | 9 + .../netty/Http3DuplexConnection.java | 146 +++++++ .../transport/netty/Http3TransportConfig.java | 113 +++++ .../transport/netty/QuicDuplexConnection.java | 161 +++++++ .../transport/netty/QuicTransportConfig.java | 171 ++++++++ .../netty/client/Http3ClientTransport.java | 67 +++ .../netty/client/QuicClientTransport.java | 77 ++++ .../internal/Http3TransportBootstrap.java | 394 ++++++++++++++++++ .../internal/QuicTransportBootstrap.java | 231 ++++++++++ .../netty/server/CloseableChannel.java | 4 + .../netty/server/Http3ServerTransport.java | 69 +++ .../netty/server/QuicServerTransport.java | 69 +++ .../netty/Http3TransportIntegrationTest.java | 164 ++++++++ .../netty/QuicTransportIntegrationTest.java | 102 +++++ .../client/Http3ClientTransportTest.java | 19 + .../netty/client/QuicClientTransportTest.java | 18 + 16 files changed, 1814 insertions(+) create mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java create mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3TransportConfig.java create mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java create mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/QuicTransportConfig.java create mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/Http3ClientTransport.java create mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/QuicClientTransport.java create mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java create mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/QuicTransportBootstrap.java create mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/Http3ServerTransport.java create mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/QuicServerTransport.java create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3TransportIntegrationTest.java create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/QuicTransportIntegrationTest.java create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/Http3ClientTransportTest.java create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/QuicClientTransportTest.java diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 39a5ceac5..f3fbc0fbd 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -26,10 +26,19 @@ if (osdetector.classifier in ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "o os_suffix = "::" + osdetector.classifier } +def netty_incubator_quic_version = "0.0.72.Final" +def netty_incubator_http3_version = "0.0.29.Final" + dependencies { api project(':rsocket-core') api "io.projectreactor.netty:reactor-netty-core" api "io.projectreactor.netty:reactor-netty-http" + implementation "io.netty.incubator:netty-incubator-codec-classes-quic:${netty_incubator_quic_version}" + implementation "io.netty.incubator:netty-incubator-codec-http3:${netty_incubator_http3_version}" + runtimeOnly "io.netty.incubator:netty-incubator-codec-native-quic:${netty_incubator_quic_version}" + if (os_suffix) { + runtimeOnly(group: "io.netty.incubator", name: "netty-incubator-codec-native-quic", version: netty_incubator_quic_version, classifier: osdetector.classifier) + } api 'org.slf4j:slf4j-api' testImplementation project(':rsocket-test') diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java new file mode 100644 index 000000000..71472cd62 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java @@ -0,0 +1,146 @@ +/* + * Copyright 2015-2021 the original author or 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.rsocket.transport.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.incubator.codec.http3.DefaultHttp3DataFrame; +import io.netty.incubator.codec.http3.Http3DataFrame; +import io.netty.incubator.codec.http3.Http3HeadersFrame; +import io.netty.incubator.codec.quic.QuicChannel; +import io.netty.incubator.codec.quic.QuicStreamChannel; +import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.internal.BaseDuplexConnection; +import io.rsocket.internal.UnboundedProcessor; +import java.nio.channels.ClosedChannelException; +import java.net.SocketAddress; +import java.util.Objects; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** An implementation of {@link DuplexConnection} that connects via HTTP/3. */ +public final class Http3DuplexConnection extends BaseDuplexConnection { + private final String side; + private final Channel connection; + private final UnboundedProcessor inbound; + + public Http3DuplexConnection(Channel connection) { + this("unknown", connection); + } + + public Http3DuplexConnection(String side, Channel connection) { + this.connection = Objects.requireNonNull(connection, "connection must not be null"); + this.side = side; + this.inbound = new UnboundedProcessor(); + + Flux.from(sender) + .concatMap( + frame -> + Mono.create( + sink -> + this.connection + .writeAndFlush(new DefaultHttp3DataFrame(frame)) + .addListener( + future -> { + if (future.isSuccess()) { + sink.success(); + } else { + sink.error(future.cause()); + } + })), + 1) + .doOnError( + throwable -> { + inbound.onError(throwable); + onClose.tryEmitError(throwable); + this.connection.close(); + }) + .doFinally( + __ -> { + if (this.connection instanceof QuicStreamChannel) { + ((QuicStreamChannel) this.connection) + .shutdownOutput() + .addListener(ChannelFutureListener.CLOSE); + } else { + this.connection.close(); + } + }) + .subscribe(); + } + + @Override + public ByteBufAllocator alloc() { + return connection.alloc(); + } + + @Override + public SocketAddress remoteAddress() { + if (connection.parent() instanceof QuicChannel) { + return ((QuicChannel) connection.parent()).remoteSocketAddress(); + } + return connection.remoteAddress(); + } + + @Override + protected void doOnClose() { + connection.close(); + } + + @Override + public Mono onClose() { + return super.onClose(); + } + + @Override + public Flux receive() { + return inbound; + } + + @Override + public void sendErrorAndClose(RSocketErrorException e) { + final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); + sender.tryEmitFinal(errorFrame); + } + + public void handleHeaders(Http3HeadersFrame frame) { + } + + public void handleData(Http3DataFrame frame) { + ByteBuf byteBuf = frame.content().retain(); + frame.release(); + inbound.onNext(byteBuf); + } + + public void handleError(Throwable cause) { + inbound.onError(cause); + onClose.tryEmitError(cause); + } + + public void handleInputClosed() { + inbound.onError(new ClosedChannelException()); + onClose.tryEmitEmpty(); + } + + @Override + public String toString() { + return "Http3DuplexConnection{" + "side='" + side + '\'' + ", connection=" + connection + '}'; + } +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3TransportConfig.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3TransportConfig.java new file mode 100644 index 000000000..5b836a136 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3TransportConfig.java @@ -0,0 +1,113 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.transport.netty; + +import java.net.URI; +import java.util.Objects; + +/** Immutable configuration for HTTP/3 based transports. */ +public final class Http3TransportConfig { + + public static final int DEFAULT_PORT = 443; + public static final String DEFAULT_PATH = "/rsocket"; + public static final String DEFAULT_METHOD = "POST"; + public static final String DEFAULT_CONTENT_TYPE = "application/rsocket"; + + private final QuicTransportConfig quicConfig; + private final String path; + private final String method; + private final String contentType; + + private Http3TransportConfig(Builder builder) { + this.quicConfig = builder.quicConfig; + this.path = builder.path; + this.method = builder.method; + this.contentType = builder.contentType; + } + + public static Http3TransportConfig create() { + return builder().build(); + } + + public static Builder builder() { + return new Builder(); + } + + public static Http3TransportConfig from(URI uri) { + final String uriPath = uri.getPath(); + return builder() + .path(uriPath == null || uriPath.isEmpty() ? DEFAULT_PATH : uriPath) + .build(); + } + + public QuicTransportConfig quicConfig() { + return quicConfig; + } + + public String path() { + return path; + } + + public String method() { + return method; + } + + public String contentType() { + return contentType; + } + + public Builder mutate() { + return builder() + .quicConfig(quicConfig) + .path(path) + .method(method) + .contentType(contentType); + } + + public static final class Builder { + private QuicTransportConfig quicConfig = + QuicTransportConfig.builder().alpn("h3").secure(true).build(); + private String path = DEFAULT_PATH; + private String method = DEFAULT_METHOD; + private String contentType = DEFAULT_CONTENT_TYPE; + + public Builder quicConfig(QuicTransportConfig quicConfig) { + this.quicConfig = Objects.requireNonNull(quicConfig, "quicConfig must not be null"); + return this; + } + + public Builder path(String path) { + Objects.requireNonNull(path, "path must not be null"); + this.path = path.startsWith("/") ? path : "/" + path; + return this; + } + + public Builder method(String method) { + this.method = Objects.requireNonNull(method, "method must not be null"); + return this; + } + + public Builder contentType(String contentType) { + this.contentType = Objects.requireNonNull(contentType, "contentType must not be null"); + return this; + } + + public Http3TransportConfig build() { + return new Http3TransportConfig(this); + } + } +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java new file mode 100644 index 000000000..48a262e36 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java @@ -0,0 +1,161 @@ +/* + * Copyright 2015-2021 the original author or 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.rsocket.transport.netty; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.incubator.codec.quic.QuicChannel; +import io.netty.incubator.codec.quic.QuicStreamChannel; +import io.rsocket.DuplexConnection; +import io.rsocket.RSocketErrorException; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.FrameLengthCodec; +import io.rsocket.internal.BaseDuplexConnection; +import io.rsocket.internal.UnboundedProcessor; +import java.nio.channels.ClosedChannelException; +import java.net.SocketAddress; +import java.util.Objects; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** An implementation of {@link DuplexConnection} that connects via QUIC. */ +public final class QuicDuplexConnection extends BaseDuplexConnection { + private final String side; + private final Channel connection; + private final UnboundedProcessor inbound; + + public QuicDuplexConnection(Channel connection) { + this("unknown", connection); + } + + public QuicDuplexConnection(String side, Channel connection) { + this.connection = Objects.requireNonNull(connection, "connection must not be null"); + this.side = side; + this.inbound = new UnboundedProcessor(); + + this.connection + .pipeline() + .addLast( + new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof ByteBuf) { + ByteBuf byteBuf = (ByteBuf) msg; + ByteBuf frame = FrameLengthCodec.frame(byteBuf).retain(); + byteBuf.release(); + inbound.onNext(frame); + } else { + ctx.fireChannelRead(msg); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + inbound.onError(cause); + onClose.tryEmitError(cause); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + inbound.onError(new ClosedChannelException()); + onClose.tryEmitEmpty(); + ctx.fireChannelInactive(); + } + }); + + Flux.from(sender) + .concatMap( + frame -> + Mono.create( + sink -> + this.connection + .writeAndFlush(frame) + .addListener( + future -> { + if (future.isSuccess()) { + sink.success(); + } else { + sink.error(future.cause()); + } + })), + 1) + .doOnError( + throwable -> { + onClose.tryEmitError(throwable); + this.connection.close(); + }) + .doFinally( + __ -> { + if (this.connection instanceof QuicStreamChannel) { + ((QuicStreamChannel) this.connection) + .shutdownOutput() + .addListener(ChannelFutureListener.CLOSE); + } else { + this.connection.close(); + } + }) + .subscribe(); + } + + @Override + public ByteBufAllocator alloc() { + return connection.alloc(); + } + + @Override + public SocketAddress remoteAddress() { + if (connection.parent() instanceof QuicChannel) { + return ((QuicChannel) connection.parent()).remoteSocketAddress(); + } + return connection.remoteAddress(); + } + + @Override + protected void doOnClose() { + connection.close(); + } + + @Override + public Mono onClose() { + return super.onClose(); + } + + @Override + public Flux receive() { + return inbound; + } + + @Override + public void sendErrorAndClose(RSocketErrorException e) { + final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); + sender.tryEmitFinal(FrameLengthCodec.encode(alloc(), errorFrame.readableBytes(), errorFrame)); + } + + @Override + public void sendFrame(int streamId, ByteBuf frame) { + super.sendFrame(streamId, FrameLengthCodec.encode(alloc(), frame.readableBytes(), frame)); + } + + @Override + public String toString() { + return "QuicDuplexConnection{" + "side='" + side + '\'' + ", connection=" + connection + '}'; + } +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/QuicTransportConfig.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/QuicTransportConfig.java new file mode 100644 index 000000000..a1e3be29b --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/QuicTransportConfig.java @@ -0,0 +1,171 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.transport.netty; + +import io.netty.incubator.codec.quic.QuicSslContext; +import java.net.URI; + +/** Immutable configuration for QUIC based transports. */ +public final class QuicTransportConfig { + + public static final int DEFAULT_PORT = 443; + public static final String DEFAULT_ALPN = "rsocket"; + + private final QuicSslContext sslContext; + private final String alpn; + private final boolean secure; + private final boolean validateCertificates; + private final long idleTimeoutMillis; + private final long maxBidirectionalStreams; + private final long maxData; + private final long maxStreamDataBidirectionalLocal; + private final long maxStreamDataBidirectionalRemote; + + private QuicTransportConfig(Builder builder) { + this.sslContext = builder.sslContext; + this.alpn = builder.alpn; + this.secure = builder.secure; + this.validateCertificates = builder.validateCertificates; + this.idleTimeoutMillis = builder.idleTimeoutMillis; + this.maxBidirectionalStreams = builder.maxBidirectionalStreams; + this.maxData = builder.maxData; + this.maxStreamDataBidirectionalLocal = builder.maxStreamDataBidirectionalLocal; + this.maxStreamDataBidirectionalRemote = builder.maxStreamDataBidirectionalRemote; + } + + public static QuicTransportConfig create() { + return builder().build(); + } + + public static Builder builder() { + return new Builder(); + } + + public static QuicTransportConfig from(URI uri) { + return builder().build(); + } + + public QuicSslContext sslContext() { + return sslContext; + } + + public String alpn() { + return alpn; + } + + public boolean secure() { + return secure; + } + + public boolean validateCertificates() { + return validateCertificates; + } + + public long idleTimeoutMillis() { + return idleTimeoutMillis; + } + + public long maxBidirectionalStreams() { + return maxBidirectionalStreams; + } + + public long maxData() { + return maxData; + } + + public long maxStreamDataBidirectionalLocal() { + return maxStreamDataBidirectionalLocal; + } + + public long maxStreamDataBidirectionalRemote() { + return maxStreamDataBidirectionalRemote; + } + + public Builder mutate() { + return builder() + .sslContext(sslContext) + .alpn(alpn) + .secure(secure) + .validateCertificates(validateCertificates) + .idleTimeoutMillis(idleTimeoutMillis) + .maxBidirectionalStreams(maxBidirectionalStreams) + .maxData(maxData) + .maxStreamDataBidirectionalLocal(maxStreamDataBidirectionalLocal) + .maxStreamDataBidirectionalRemote(maxStreamDataBidirectionalRemote); + } + + public static final class Builder { + private QuicSslContext sslContext; + private String alpn = DEFAULT_ALPN; + private boolean secure = true; + private boolean validateCertificates = true; + private long idleTimeoutMillis = 30_000; + private long maxBidirectionalStreams = 1; + private long maxData = 10_000_000; + private long maxStreamDataBidirectionalLocal = 1_000_000; + private long maxStreamDataBidirectionalRemote = 1_000_000; + + public Builder sslContext(QuicSslContext sslContext) { + this.sslContext = sslContext; + return this; + } + + public Builder alpn(String alpn) { + this.alpn = alpn; + return this; + } + + public Builder secure(boolean secure) { + this.secure = secure; + return this; + } + + public Builder validateCertificates(boolean validateCertificates) { + this.validateCertificates = validateCertificates; + return this; + } + + public Builder idleTimeoutMillis(long idleTimeoutMillis) { + this.idleTimeoutMillis = idleTimeoutMillis; + return this; + } + + public Builder maxBidirectionalStreams(long maxBidirectionalStreams) { + this.maxBidirectionalStreams = maxBidirectionalStreams; + return this; + } + + public Builder maxData(long maxData) { + this.maxData = maxData; + return this; + } + + public Builder maxStreamDataBidirectionalLocal(long maxStreamDataBidirectionalLocal) { + this.maxStreamDataBidirectionalLocal = maxStreamDataBidirectionalLocal; + return this; + } + + public Builder maxStreamDataBidirectionalRemote(long maxStreamDataBidirectionalRemote) { + this.maxStreamDataBidirectionalRemote = maxStreamDataBidirectionalRemote; + return this; + } + + public QuicTransportConfig build() { + return new QuicTransportConfig(this); + } + } +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/Http3ClientTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/Http3ClientTransport.java new file mode 100644 index 000000000..ec3cf6426 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/Http3ClientTransport.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.transport.netty.client; + +import io.rsocket.DuplexConnection; +import io.rsocket.transport.ClientTransport; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.Http3TransportConfig; +import io.rsocket.transport.netty.internal.Http3TransportBootstrap; +import java.net.URI; +import java.util.Objects; +import reactor.core.publisher.Mono; + +/** + * An implementation of {@link ClientTransport} that connects to a {@link ServerTransport} over + * HTTP/3. + */ +public final class Http3ClientTransport implements ClientTransport { + + private final String host; + private final int port; + private final Http3TransportConfig config; + + private Http3ClientTransport(String host, int port, Http3TransportConfig config) { + this.host = Objects.requireNonNull(host, "host must not be null"); + this.port = port; + this.config = Objects.requireNonNull(config, "config must not be null"); + } + + public static Http3ClientTransport create(String host, int port) { + return new Http3ClientTransport(host, port, Http3TransportConfig.create()); + } + + public static Http3ClientTransport create(URI uri) { + Objects.requireNonNull(uri, "uri must not be null"); + return new Http3ClientTransport( + uri.getHost(), uri.getPort() == -1 ? Http3TransportConfig.DEFAULT_PORT : uri.getPort(), + Http3TransportConfig.from(uri)); + } + + public Http3ClientTransport config(Http3TransportConfig config) { + return new Http3ClientTransport(host, port, config); + } + + public Http3TransportConfig configuration() { + return config; + } + + @Override + public Mono connect() { + return Http3TransportBootstrap.connectClient(host, port, config); + } +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/QuicClientTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/QuicClientTransport.java new file mode 100644 index 000000000..d5aac5df9 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/QuicClientTransport.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.transport.netty.client; + +import io.rsocket.DuplexConnection; +import io.rsocket.transport.ClientTransport; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.QuicTransportConfig; +import io.rsocket.transport.netty.internal.QuicTransportBootstrap; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Objects; +import reactor.core.publisher.Mono; + +/** + * An implementation of {@link ClientTransport} that connects to a {@link ServerTransport} via + * QUIC. + */ +public final class QuicClientTransport implements ClientTransport { + + private final String host; + private final int port; + private final QuicTransportConfig config; + + private QuicClientTransport(String host, int port, QuicTransportConfig config) { + this.host = Objects.requireNonNull(host, "host must not be null"); + this.port = port; + this.config = Objects.requireNonNull(config, "config must not be null"); + } + + public static QuicClientTransport create(int port) { + return create("localhost", port); + } + + public static QuicClientTransport create(String bindAddress, int port) { + return new QuicClientTransport(bindAddress, port, QuicTransportConfig.create()); + } + + public static QuicClientTransport create(InetSocketAddress address) { + Objects.requireNonNull(address, "address must not be null"); + return create(address.getHostString(), address.getPort()); + } + + public static QuicClientTransport create(URI uri) { + Objects.requireNonNull(uri, "uri must not be null"); + return new QuicClientTransport( + uri.getHost(), uri.getPort() == -1 ? QuicTransportConfig.DEFAULT_PORT : uri.getPort(), + QuicTransportConfig.from(uri)); + } + + public QuicClientTransport config(QuicTransportConfig config) { + return new QuicClientTransport(host, port, config); + } + + public QuicTransportConfig configuration() { + return config; + } + + @Override + public Mono connect() { + return QuicTransportBootstrap.connectClient(host, port, config); + } +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java new file mode 100644 index 000000000..ceff418a2 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java @@ -0,0 +1,394 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.transport.netty.internal; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.incubator.codec.http3.DefaultHttp3HeadersFrame; +import io.netty.incubator.codec.http3.Http3; +import io.netty.incubator.codec.http3.Http3ClientConnectionHandler; +import io.netty.incubator.codec.http3.Http3DataFrame; +import io.netty.incubator.codec.http3.Http3HeadersFrame; +import io.netty.incubator.codec.http3.Http3RequestStreamInboundHandler; +import io.netty.incubator.codec.http3.Http3ServerConnectionHandler; +import io.netty.incubator.codec.quic.InsecureQuicTokenHandler; +import io.netty.incubator.codec.quic.Quic; +import io.netty.incubator.codec.quic.QuicChannel; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; +import io.netty.incubator.codec.quic.QuicStreamChannel; +import io.rsocket.DuplexConnection; +import io.rsocket.transport.ServerTransport.ConnectionAcceptor; +import io.rsocket.transport.netty.Http3DuplexConnection; +import io.rsocket.transport.netty.Http3TransportConfig; +import io.rsocket.transport.netty.server.CloseableChannel; +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; +import reactor.core.publisher.Sinks; +import reactor.core.publisher.Mono; +import reactor.netty.Connection; + +public final class Http3TransportBootstrap { + + private Http3TransportBootstrap() {} + + public static Mono connectClient( + String host, int port, Http3TransportConfig config) { + return Mono.create( + sink -> { + Quic.ensureAvailability(); + + final EventLoopGroup group = new NioEventLoopGroup(1); + final ChannelFuture bindFuture; + try { + bindFuture = + new Bootstrap() + .group(group) + .channel(NioDatagramChannel.class) + .handler( + Http3.newQuicClientCodecBuilder() + .sslContext(createClientSslContext(config)) + .maxIdleTimeout( + config.quicConfig().idleTimeoutMillis(), TimeUnit.MILLISECONDS) + .initialMaxData(config.quicConfig().maxData()) + .initialMaxStreamDataBidirectionalLocal( + config.quicConfig().maxStreamDataBidirectionalLocal()) + .initialMaxStreamDataBidirectionalRemote( + config.quicConfig().maxStreamDataBidirectionalRemote()) + .build()) + .bind(0); + } catch (Exception e) { + group.shutdownGracefully(); + sink.error(e); + return; + } + + bindFuture.addListener( + future -> { + if (!future.isSuccess()) { + group.shutdownGracefully(); + sink.error(future.cause()); + return; + } + + final Channel datagramChannel = ((ChannelFuture) future).channel(); + QuicChannel.newBootstrap(datagramChannel) + .handler(new Http3ClientConnectionHandler()) + .remoteAddress(new InetSocketAddress(host, port)) + .connect() + .addListener( + quicFuture -> { + if (!quicFuture.isSuccess()) { + datagramChannel.close(); + group.shutdownGracefully(); + sink.error(quicFuture.cause()); + return; + } + + final QuicChannel quicChannel = (QuicChannel) quicFuture.getNow(); + final Http3DuplexConnection[] holder = new Http3DuplexConnection[1]; + final Sinks.One established = Sinks.one(); + + Http3.newRequestStream( + quicChannel, + new Http3RequestStreamInboundHandler() { + @Override + protected void channelRead( + ChannelHandlerContext ctx, Http3HeadersFrame frame) { + if (!isSuccessful(frame)) { + IllegalStateException error = + new IllegalStateException( + "HTTP/3 transport rejected with status " + + frame.headers().status()); + established.tryEmitError(error); + if (holder[0] != null) { + holder[0].handleError(error); + } + ctx.close(); + return; + } + + if (holder[0] != null) { + established.tryEmitValue(holder[0]); + holder[0].handleHeaders(frame); + } + } + + @Override + protected void channelRead( + ChannelHandlerContext ctx, Http3DataFrame frame) { + if (holder[0] != null) { + holder[0].handleData(frame); + } else { + frame.release(); + } + } + + @Override + protected void channelInputClosed(ChannelHandlerContext ctx) { + if (holder[0] != null) { + holder[0].handleInputClosed(); + } + established.tryEmitError( + new IllegalStateException( + "HTTP/3 transport closed before handshake completed")); + } + + @Override + public void exceptionCaught( + ChannelHandlerContext ctx, Throwable cause) { + established.tryEmitError(cause); + if (holder[0] != null) { + holder[0].handleError(cause); + } else { + sink.error(cause); + } + } + }) + .addListener( + streamFuture -> { + if (!streamFuture.isSuccess()) { + quicChannel.close(); + datagramChannel.close(); + group.shutdownGracefully(); + sink.error(streamFuture.cause()); + return; + } + + final QuicStreamChannel streamChannel = + (QuicStreamChannel) streamFuture.getNow(); + final Http3DuplexConnection connection = + new Http3DuplexConnection("client", streamChannel); + holder[0] = connection; + + sendRequestHeaders(streamChannel, host, port, config) + .addListener( + requestFuture -> { + if (!requestFuture.isSuccess()) { + quicChannel.close(); + datagramChannel.close(); + group.shutdownGracefully(); + sink.error(requestFuture.cause()); + return; + } + established + .asMono() + .subscribe( + sink::success, + error -> { + quicChannel.close(); + datagramChannel.close(); + group.shutdownGracefully(); + sink.error(error); + }); + }); + + streamChannel + .closeFuture() + .addListener( + __ -> { + quicChannel.close(); + datagramChannel.close(); + group.shutdownGracefully(); + }); + }); + }); + }); + }); + } + + public static Mono bindServer( + String bindAddress, int port, Http3TransportConfig config, ConnectionAcceptor acceptor) { + return Mono.create( + sink -> { + Quic.ensureAvailability(); + + final EventLoopGroup group = new NioEventLoopGroup(1); + final ChannelFuture bindFuture; + try { + bindFuture = + new Bootstrap() + .group(group) + .channel(NioDatagramChannel.class) + .handler( + Http3.newQuicServerCodecBuilder() + .sslContext(createServerSslContext(config)) + .maxIdleTimeout( + config.quicConfig().idleTimeoutMillis(), TimeUnit.MILLISECONDS) + .initialMaxData(config.quicConfig().maxData()) + .initialMaxStreamDataBidirectionalLocal( + config.quicConfig().maxStreamDataBidirectionalLocal()) + .initialMaxStreamDataBidirectionalRemote( + config.quicConfig().maxStreamDataBidirectionalRemote()) + .initialMaxStreamsBidirectional( + config.quicConfig().maxBidirectionalStreams()) + .tokenHandler(InsecureQuicTokenHandler.INSTANCE) + .handler( + new ChannelInitializer() { + @Override + protected void initChannel(QuicChannel ch) { + ch.pipeline() + .addLast( + new Http3ServerConnectionHandler( + new ChannelInitializer() { + @Override + protected void initChannel( + QuicStreamChannel ch) { + final Http3DuplexConnection connection = + new Http3DuplexConnection("server", ch); + ch.pipeline() + .addLast( + new ServerStreamHandler( + config, connection, acceptor)); + } + })); + } + }) + .build()) + .bind(new InetSocketAddress(bindAddress, port)); + } catch (Exception e) { + group.shutdownGracefully(); + sink.error(e); + return; + } + + bindFuture.addListener( + future -> { + if (!future.isSuccess()) { + group.shutdownGracefully(); + sink.error(future.cause()); + return; + } + + final Channel datagramChannel = ((ChannelFuture) future).channel(); + datagramChannel.closeFuture().addListener(__ -> group.shutdownGracefully()); + sink.success(CloseableChannel.from(Connection.from(datagramChannel))); + }); + }); + } + + private static QuicSslContext createClientSslContext(Http3TransportConfig config) throws Exception { + if (config.quicConfig().sslContext() != null) { + return config.quicConfig().sslContext(); + } + + QuicSslContextBuilder builder = + QuicSslContextBuilder.forClient().applicationProtocols(Http3.supportedApplicationProtocols()); + if (!config.quicConfig().validateCertificates()) { + builder.trustManager(io.netty.handler.ssl.util.InsecureTrustManagerFactory.INSTANCE); + } + return builder.build(); + } + + private static QuicSslContext createServerSslContext(Http3TransportConfig config) { + if (config.quicConfig().sslContext() == null) { + throw new IllegalStateException( + "HTTP/3 server requires an explicit QuicSslContext configured via Http3TransportConfig"); + } + return config.quicConfig().sslContext(); + } + + private static ChannelFuture sendRequestHeaders( + QuicStreamChannel streamChannel, String host, int port, Http3TransportConfig config) { + DefaultHttp3HeadersFrame frame = new DefaultHttp3HeadersFrame(); + frame.headers() + .method(config.method()) + .path(config.path()) + .authority(host + ":" + port) + .scheme("https") + .add("content-type", config.contentType()); + return streamChannel.writeAndFlush(frame); + } + + private static boolean isSuccessful(Http3HeadersFrame frame) { + CharSequence status = frame.headers().status(); + return status != null && status.length() > 0 && status.charAt(0) == '2'; + } + + private static boolean matches(Http3HeadersFrame frame, Http3TransportConfig config) { + CharSequence method = frame.headers().method(); + CharSequence path = frame.headers().path(); + CharSequence contentType = frame.headers().get("content-type"); + return method != null + && path != null + && config.method().contentEquals(method) + && config.path().contentEquals(path) + && (contentType == null || config.contentType().contentEquals(contentType)); + } + + private static final class ServerStreamHandler extends Http3RequestStreamInboundHandler { + private final Http3TransportConfig config; + private final Http3DuplexConnection connection; + private final ConnectionAcceptor acceptor; + private boolean established; + + private ServerStreamHandler( + Http3TransportConfig config, Http3DuplexConnection connection, ConnectionAcceptor acceptor) { + this.config = config; + this.connection = connection; + this.acceptor = acceptor; + } + + @Override + protected void channelRead(ChannelHandlerContext ctx, Http3HeadersFrame frame) { + if (!established) { + if (!matches(frame, config)) { + DefaultHttp3HeadersFrame response = new DefaultHttp3HeadersFrame(); + response.headers().status("404"); + ctx.writeAndFlush(response).addListener(QuicStreamChannel.SHUTDOWN_OUTPUT); + ctx.close(); + return; + } + + DefaultHttp3HeadersFrame response = new DefaultHttp3HeadersFrame(); + response.headers().status("200").add("content-type", config.contentType()); + ctx.writeAndFlush(response); + established = true; + acceptor.apply(connection).doFinally(__ -> ctx.close()).subscribe(null, t -> ctx.close()); + return; + } + + connection.handleHeaders(frame); + } + + @Override + protected void channelRead(ChannelHandlerContext ctx, Http3DataFrame frame) { + if (established) { + connection.handleData(frame); + } else { + frame.release(); + } + } + + @Override + protected void channelInputClosed(ChannelHandlerContext ctx) { + connection.handleInputClosed(); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + connection.handleError(cause); + ctx.close(); + } + } +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/QuicTransportBootstrap.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/QuicTransportBootstrap.java new file mode 100644 index 000000000..37ba20219 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/QuicTransportBootstrap.java @@ -0,0 +1,231 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.transport.netty.internal; + +import io.netty.bootstrap.Bootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.incubator.codec.quic.InsecureQuicTokenHandler; +import io.netty.incubator.codec.quic.Quic; +import io.netty.incubator.codec.quic.QuicChannel; +import io.netty.incubator.codec.quic.QuicClientCodecBuilder; +import io.netty.incubator.codec.quic.QuicServerCodecBuilder; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; +import io.netty.incubator.codec.quic.QuicStreamChannel; +import io.netty.incubator.codec.quic.QuicStreamType; +import io.rsocket.DuplexConnection; +import io.rsocket.transport.ServerTransport.ConnectionAcceptor; +import io.rsocket.transport.netty.QuicDuplexConnection; +import io.rsocket.transport.netty.QuicTransportConfig; +import io.rsocket.transport.netty.server.CloseableChannel; +import java.net.InetSocketAddress; +import java.util.concurrent.TimeUnit; +import reactor.netty.Connection; +import reactor.core.publisher.Mono; + +public final class QuicTransportBootstrap { + + private QuicTransportBootstrap() {} + + public static Mono connectClient( + String host, int port, QuicTransportConfig config) { + return Mono.create( + sink -> { + Quic.ensureAvailability(); + + final EventLoopGroup group = new NioEventLoopGroup(1); + final ChannelHandler codec; + try { + codec = + new QuicClientCodecBuilder() + .sslContext(createClientSslContext(config)) + .maxIdleTimeout(config.idleTimeoutMillis(), TimeUnit.MILLISECONDS) + .initialMaxData(config.maxData()) + .initialMaxStreamDataBidirectionalLocal(config.maxStreamDataBidirectionalLocal()) + .initialMaxStreamDataBidirectionalRemote(config.maxStreamDataBidirectionalRemote()) + .build(); + } catch (Exception e) { + group.shutdownGracefully(); + sink.error(e); + return; + } + + new Bootstrap() + .group(group) + .channel(NioDatagramChannel.class) + .handler(codec) + .bind(0) + .addListener( + bindFuture -> { + if (!bindFuture.isSuccess()) { + group.shutdownGracefully(); + sink.error(bindFuture.cause()); + return; + } + + final Channel datagramChannel = ((ChannelFuture) bindFuture).channel(); + QuicChannel.newBootstrap(datagramChannel) + .handler(NOOP_HANDLER) + .streamHandler(NOOP_HANDLER) + .remoteAddress(new InetSocketAddress(host, port)) + .connect() + .addListener( + quicFuture -> { + if (!quicFuture.isSuccess()) { + datagramChannel.close(); + group.shutdownGracefully(); + sink.error(quicFuture.cause()); + return; + } + + final QuicChannel quicChannel = (QuicChannel) quicFuture.getNow(); + quicChannel + .createStream(QuicStreamType.BIDIRECTIONAL, null) + .addListener( + streamFuture -> { + if (!streamFuture.isSuccess()) { + quicChannel.close(); + datagramChannel.close(); + group.shutdownGracefully(); + sink.error(streamFuture.cause()); + return; + } + + final QuicStreamChannel streamChannel = + (QuicStreamChannel) streamFuture.getNow(); + + final DuplexConnection connection = + new QuicDuplexConnection("client", streamChannel); + + streamChannel + .closeFuture() + .addListener( + __ -> { + quicChannel.close(); + datagramChannel.close(); + group.shutdownGracefully(); + }); + + sink.success(connection); + }); + }); + }); + }); + } + + public static Mono bindServer( + String bindAddress, int port, QuicTransportConfig config, ConnectionAcceptor acceptor) { + return Mono.create( + sink -> { + Quic.ensureAvailability(); + + final EventLoopGroup group = new NioEventLoopGroup(1); + final ChannelHandler codec; + try { + codec = + new QuicServerCodecBuilder() + .sslContext(createServerSslContext(config)) + .maxIdleTimeout(config.idleTimeoutMillis(), TimeUnit.MILLISECONDS) + .initialMaxData(config.maxData()) + .initialMaxStreamDataBidirectionalLocal(config.maxStreamDataBidirectionalLocal()) + .initialMaxStreamDataBidirectionalRemote(config.maxStreamDataBidirectionalRemote()) + .initialMaxStreamsBidirectional(config.maxBidirectionalStreams()) + .tokenHandler(InsecureQuicTokenHandler.INSTANCE) + .handler(NOOP_HANDLER) + .streamHandler( + new ChannelInitializer() { + @Override + protected void initChannel(QuicStreamChannel ch) { + final DuplexConnection connection = + new QuicDuplexConnection("server", ch); + acceptor + .apply(connection) + .doFinally(__ -> ch.close()) + .subscribe(null, t -> ch.close()); + } + }) + .build(); + } catch (Exception e) { + group.shutdownGracefully(); + sink.error(e); + return; + } + + new Bootstrap() + .group(group) + .channel(NioDatagramChannel.class) + .handler(codec) + .bind(new InetSocketAddress(bindAddress, port)) + .addListener( + bindFuture -> { + if (!bindFuture.isSuccess()) { + group.shutdownGracefully(); + sink.error(bindFuture.cause()); + return; + } + + final Channel datagramChannel = ((ChannelFuture) bindFuture).channel(); + datagramChannel + .closeFuture() + .addListener(__ -> group.shutdownGracefully()); + + sink.success(CloseableChannel.from(Connection.from(datagramChannel))); + }); + }); + } + + private static QuicSslContext createClientSslContext(QuicTransportConfig config) throws Exception { + if (config.sslContext() != null) { + return config.sslContext(); + } + + final QuicSslContextBuilder builder = + QuicSslContextBuilder.forClient().applicationProtocols(config.alpn()); + if (!config.validateCertificates()) { + builder.trustManager(InsecureTrustManagerFactory.INSTANCE); + } + return builder.build(); + } + + private static QuicSslContext createServerSslContext(QuicTransportConfig config) { + if (config.sslContext() == null) { + throw new IllegalStateException( + "QUIC server requires an explicit QuicSslContext configured via QuicTransportConfig"); + } + return config.sslContext(); + } + + private static final ChannelInboundHandlerAdapter NOOP_HANDLER = + new ChannelInboundHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + + @Override + public void channelActive(ChannelHandlerContext ctx) {} + }; +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/CloseableChannel.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/CloseableChannel.java index 7e98905ff..1f58714ce 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/CloseableChannel.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/CloseableChannel.java @@ -52,6 +52,10 @@ public final class CloseableChannel implements Closeable { this.channel = Objects.requireNonNull(channel, "channel must not be null"); } + public static CloseableChannel from(DisposableChannel channel) { + return new CloseableChannel(channel); + } + /** * Return local server selector channel address. * diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/Http3ServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/Http3ServerTransport.java new file mode 100644 index 000000000..b85a8e6f2 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/Http3ServerTransport.java @@ -0,0 +1,69 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.transport.netty.server; + +import io.rsocket.transport.ClientTransport; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.Http3TransportConfig; +import io.rsocket.transport.netty.internal.Http3TransportBootstrap; +import java.net.InetSocketAddress; +import java.util.Objects; +import reactor.core.publisher.Mono; + +/** + * An implementation of {@link ServerTransport} that connects to a {@link ClientTransport} over + * HTTP/3. + */ +public final class Http3ServerTransport implements ServerTransport { + + private final String bindAddress; + private final int port; + private final Http3TransportConfig config; + + private Http3ServerTransport(String bindAddress, int port, Http3TransportConfig config) { + this.bindAddress = Objects.requireNonNull(bindAddress, "bindAddress must not be null"); + this.port = port; + this.config = Objects.requireNonNull(config, "config must not be null"); + } + + public static Http3ServerTransport create(int port) { + return create("localhost", port); + } + + public static Http3ServerTransport create(String bindAddress, int port) { + return new Http3ServerTransport(bindAddress, port, Http3TransportConfig.create()); + } + + public static Http3ServerTransport create(InetSocketAddress address) { + Objects.requireNonNull(address, "address must not be null"); + return create(address.getHostString(), address.getPort()); + } + + public Http3ServerTransport config(Http3TransportConfig config) { + return new Http3ServerTransport(bindAddress, port, config); + } + + public Http3TransportConfig configuration() { + return config; + } + + @Override + public Mono start(ConnectionAcceptor acceptor) { + Objects.requireNonNull(acceptor, "acceptor must not be null"); + return Http3TransportBootstrap.bindServer(bindAddress, port, config, acceptor); + } +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/QuicServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/QuicServerTransport.java new file mode 100644 index 000000000..3d786565d --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/QuicServerTransport.java @@ -0,0 +1,69 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.transport.netty.server; + +import io.rsocket.transport.ClientTransport; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.QuicTransportConfig; +import io.rsocket.transport.netty.internal.QuicTransportBootstrap; +import java.net.InetSocketAddress; +import java.util.Objects; +import reactor.core.publisher.Mono; + +/** + * An implementation of {@link ServerTransport} that connects to a {@link ClientTransport} via + * QUIC. + */ +public final class QuicServerTransport implements ServerTransport { + + private final String bindAddress; + private final int port; + private final QuicTransportConfig config; + + private QuicServerTransport(String bindAddress, int port, QuicTransportConfig config) { + this.bindAddress = Objects.requireNonNull(bindAddress, "bindAddress must not be null"); + this.port = port; + this.config = Objects.requireNonNull(config, "config must not be null"); + } + + public static QuicServerTransport create(int port) { + return create("localhost", port); + } + + public static QuicServerTransport create(String bindAddress, int port) { + return new QuicServerTransport(bindAddress, port, QuicTransportConfig.create()); + } + + public static QuicServerTransport create(InetSocketAddress address) { + Objects.requireNonNull(address, "address must not be null"); + return create(address.getHostString(), address.getPort()); + } + + public QuicServerTransport config(QuicTransportConfig config) { + return new QuicServerTransport(bindAddress, port, config); + } + + public QuicTransportConfig configuration() { + return config; + } + + @Override + public Mono start(ConnectionAcceptor acceptor) { + Objects.requireNonNull(acceptor, "acceptor must not be null"); + return QuicTransportBootstrap.bindServer(bindAddress, port, config, acceptor); + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3TransportIntegrationTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3TransportIntegrationTest.java new file mode 100644 index 000000000..612e8a14c --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3TransportIntegrationTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2015-2024 the original author or 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.rsocket.transport.netty; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.incubator.codec.http3.Http3; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; +import io.rsocket.DuplexConnection; +import io.rsocket.transport.netty.client.Http3ClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.Http3ServerTransport; +import java.time.Duration; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Sinks; + +class Http3TransportIntegrationTest { + + @Test + void shouldConnectAndExchangeFrames() throws Exception { + SelfSignedCertificate certificate = new SelfSignedCertificate(); + QuicSslContext serverSslContext = + QuicSslContextBuilder.forServer( + certificate.privateKey(), null, certificate.certificate()) + .applicationProtocols(Http3.supportedApplicationProtocols()) + .build(); + QuicSslContext clientSslContext = + QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(Http3.supportedApplicationProtocols()) + .build(); + + Http3TransportConfig serverConfig = + Http3TransportConfig.builder() + .path("/rsocket") + .quicConfig(QuicTransportConfig.builder().sslContext(serverSslContext).build()) + .build(); + Http3TransportConfig clientConfig = + Http3TransportConfig.builder() + .path("/rsocket") + .quicConfig( + QuicTransportConfig.builder() + .sslContext(clientSslContext) + .validateCertificates(false) + .build()) + .build(); + + Sinks.One accepted = Sinks.one(); + CloseableChannel server = + Http3ServerTransport.create(0) + .config(serverConfig) + .start( + connection -> { + accepted.tryEmitValue(connection); + return connection.onClose(); + }) + .block(Duration.ofSeconds(10)); + + DuplexConnection client = + Http3ClientTransport.create("127.0.0.1", server.address().getPort()) + .config(clientConfig) + .connect() + .block(Duration.ofSeconds(10)); + + DuplexConnection serverConnection = accepted.asMono().block(Duration.ofSeconds(10)); + + client.sendFrame(1, Unpooled.wrappedBuffer(new byte[] {4, 3, 2, 1})); + ByteBuf serverReceived = serverConnection.receive().next().block(Duration.ofSeconds(10)); + try { + assertThat(serverReceived.readableBytes()).isEqualTo(4); + assertThat(serverReceived.readByte()).isEqualTo((byte) 4); + assertThat(serverReceived.readByte()).isEqualTo((byte) 3); + assertThat(serverReceived.readByte()).isEqualTo((byte) 2); + assertThat(serverReceived.readByte()).isEqualTo((byte) 1); + } finally { + serverReceived.release(); + } + + serverConnection.sendFrame(1, Unpooled.wrappedBuffer(new byte[] {7, 8, 9, 10})); + ByteBuf clientReceived = client.receive().next().block(Duration.ofSeconds(10)); + try { + assertThat(clientReceived.readableBytes()).isEqualTo(4); + assertThat(clientReceived.readByte()).isEqualTo((byte) 7); + assertThat(clientReceived.readByte()).isEqualTo((byte) 8); + assertThat(clientReceived.readByte()).isEqualTo((byte) 9); + assertThat(clientReceived.readByte()).isEqualTo((byte) 10); + } finally { + clientReceived.release(); + client.dispose(); + serverConnection.dispose(); + server.dispose(); + certificate.delete(); + } + } + + @Test + void shouldFailConnectWhenServerRejectsHandshake() throws Exception { + SelfSignedCertificate certificate = new SelfSignedCertificate(); + QuicSslContext serverSslContext = + QuicSslContextBuilder.forServer( + certificate.privateKey(), null, certificate.certificate()) + .applicationProtocols(Http3.supportedApplicationProtocols()) + .build(); + QuicSslContext clientSslContext = + QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(Http3.supportedApplicationProtocols()) + .build(); + + Http3TransportConfig serverConfig = + Http3TransportConfig.builder() + .path("/rsocket") + .quicConfig(QuicTransportConfig.builder().sslContext(serverSslContext).build()) + .build(); + Http3TransportConfig clientConfig = + Http3TransportConfig.builder() + .path("/wrong") + .quicConfig( + QuicTransportConfig.builder() + .sslContext(clientSslContext) + .validateCertificates(false) + .build()) + .build(); + + CloseableChannel server = + Http3ServerTransport.create(0) + .config(serverConfig) + .start(connection -> connection.onClose()) + .block(Duration.ofSeconds(10)); + + try { + assertThatThrownBy( + () -> + Http3ClientTransport.create("127.0.0.1", server.address().getPort()) + .config(clientConfig) + .connect() + .block(Duration.ofSeconds(10))) + .hasMessageContaining("HTTP/3 transport rejected with status 404"); + } finally { + server.dispose(); + certificate.delete(); + } + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/QuicTransportIntegrationTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/QuicTransportIntegrationTest.java new file mode 100644 index 000000000..340a82e21 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/QuicTransportIntegrationTest.java @@ -0,0 +1,102 @@ +/* + * Copyright 2015-2024 the original author or 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.rsocket.transport.netty; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; +import io.rsocket.DuplexConnection; +import io.rsocket.transport.netty.client.QuicClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.QuicServerTransport; +import java.time.Duration; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Sinks; + +class QuicTransportIntegrationTest { + + @Test + void shouldConnectAndExchangeFrames() throws Exception { + SelfSignedCertificate certificate = new SelfSignedCertificate(); + QuicSslContext serverSslContext = + QuicSslContextBuilder.forServer( + certificate.privateKey(), null, certificate.certificate()) + .applicationProtocols("rsocket") + .build(); + QuicSslContext clientSslContext = + QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols("rsocket") + .build(); + + Sinks.One accepted = Sinks.one(); + CloseableChannel server = + QuicServerTransport.create(0) + .config(QuicTransportConfig.builder().sslContext(serverSslContext).build()) + .start( + connection -> { + accepted.tryEmitValue(connection); + return connection.onClose(); + }) + .block(Duration.ofSeconds(10)); + + DuplexConnection client = + QuicClientTransport.create("127.0.0.1", server.address().getPort()) + .config( + QuicTransportConfig.builder() + .sslContext(clientSslContext) + .validateCertificates(false) + .build()) + .connect() + .block(Duration.ofSeconds(10)); + + client.sendFrame(1, Unpooled.wrappedBuffer(new byte[] {9, 8, 7, 6})); + DuplexConnection serverConnection = accepted.asMono().block(Duration.ofSeconds(10)); + + ByteBuf serverReceived = serverConnection.receive().next().block(Duration.ofSeconds(10)); + try { + assertThat(serverReceived.readableBytes()).isEqualTo(4); + assertThat(serverReceived.readByte()).isEqualTo((byte) 9); + assertThat(serverReceived.readByte()).isEqualTo((byte) 8); + assertThat(serverReceived.readByte()).isEqualTo((byte) 7); + assertThat(serverReceived.readByte()).isEqualTo((byte) 6); + } finally { + serverReceived.release(); + } + + serverConnection.sendFrame(1, Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4})); + ByteBuf received = client.receive().next().block(Duration.ofSeconds(10)); + try { + assertThat(received.readableBytes()).isEqualTo(4); + assertThat(received.readByte()).isEqualTo((byte) 1); + assertThat(received.readByte()).isEqualTo((byte) 2); + assertThat(received.readByte()).isEqualTo((byte) 3); + assertThat(received.readByte()).isEqualTo((byte) 4); + } finally { + received.release(); + client.dispose(); + serverConnection.dispose(); + server.dispose(); + certificate.delete(); + } + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/Http3ClientTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/Http3ClientTransportTest.java new file mode 100644 index 000000000..d13a67837 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/Http3ClientTransportTest.java @@ -0,0 +1,19 @@ +package io.rsocket.transport.netty.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.rsocket.transport.netty.Http3TransportConfig; +import java.net.URI; +import org.junit.jupiter.api.Test; + +class Http3ClientTransportTest { + + @Test + void shouldDerivePathFromUri() { + Http3ClientTransport transport = Http3ClientTransport.create(URI.create("h3://localhost/demo")); + + assertThat(transport.configuration().path()).isEqualTo("/demo"); + assertThat(transport.configuration().contentType()) + .isEqualTo(Http3TransportConfig.DEFAULT_CONTENT_TYPE); + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/QuicClientTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/QuicClientTransportTest.java new file mode 100644 index 000000000..f9a302696 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/QuicClientTransportTest.java @@ -0,0 +1,18 @@ +package io.rsocket.transport.netty.client; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.rsocket.transport.netty.QuicTransportConfig; +import java.net.URI; +import org.junit.jupiter.api.Test; + +class QuicClientTransportTest { + + @Test + void shouldApplyQuicDefaultsFromUri() { + QuicClientTransport transport = QuicClientTransport.create(URI.create("quic://localhost")); + + assertThat(transport.configuration().secure()).isTrue(); + assertThat(transport.configuration().alpn()).isEqualTo(QuicTransportConfig.DEFAULT_ALPN); + } +} From 08b2f47ba33b7f9b6f0092d48cdab96c11580f38 Mon Sep 17 00:00:00 2001 From: "jeroen.veltman" Date: Tue, 31 Mar 2026 12:29:34 +0200 Subject: [PATCH 02/11] Document HTTP/3 transport contract and bump incubator deps Signed-off-by: jeroen.veltman --- .../QUIC_HTTP3_TRANSPORTS.md | 44 +++++++++++++++++++ rsocket-transport-netty/build.gradle | 4 +- .../internal/Http3TransportBootstrap.java | 1 - 3 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 rsocket-transport-netty/QUIC_HTTP3_TRANSPORTS.md diff --git a/rsocket-transport-netty/QUIC_HTTP3_TRANSPORTS.md b/rsocket-transport-netty/QUIC_HTTP3_TRANSPORTS.md new file mode 100644 index 000000000..64420c6e6 --- /dev/null +++ b/rsocket-transport-netty/QUIC_HTTP3_TRANSPORTS.md @@ -0,0 +1,44 @@ +# QUIC and HTTP/3 transport notes + +This module now contains prototype Netty transports for: + +- `quic://` via `QuicClientTransport` and `QuicServerTransport` +- `h3://` via `Http3ClientTransport` and `Http3ServerTransport` + +## Current HTTP/3 transport contract + +The current `h3://` transport uses a long-lived HTTP/3 `POST` request stream: + +- path defaults to `/rsocket` +- content type defaults to `application/rsocket` +- the request body carries raw RSocket frames in both directions once the server accepts the stream +- `Http3ClientTransport.connect()` completes only after the client receives a successful response headers frame + +This is intentionally narrower than a general-purpose HTTP/3 tunnel, but it is the shape that is currently proven by the local integration tests in this repository. + +## Why the transport still uses `POST` + +An investigation was done into moving the transport to HTTP/3 `CONNECT`, including: + +- switching the request method from `POST` to `CONNECT` +- enabling the HTTP/3 `ENABLE_CONNECT_PROTOCOL` setting on both peers +- trying the newer Netty incubator pair `netty-incubator-codec-http3:0.0.30.Final` and `netty-incubator-codec-classes-quic:0.0.73.Final` +- attempting an extended `CONNECT` request shape with the `:protocol` pseudo-header + +Those experiments still caused the request stream to close before the client observed a successful handshake response, so they were not kept in the working transport implementation. + +## Upstream findings that matter + +The current Netty HTTP/3 codec line does model both regular and extended `CONNECT`, but the details are stricter than a simple method flip: + +- regular `CONNECT` expects only `:method` and `:authority` +- extended `CONNECT` expects `:method`, `:scheme`, `:authority`, `:path`, and `:protocol` +- `ENABLE_CONNECT_PROTOCOL` is negotiated through HTTP/3 settings and is disabled by default + +In practice, that means a future `CONNECT`-based RSocket transport likely needs a more complete negotiation flow than the current prototype has. + +## Good next steps + +- keep `POST` as the default transport contract until the `CONNECT` path is validated end-to-end +- if `CONNECT` is revisited, start with a dedicated experiment around response-header timing and remote settings negotiation +- add a focused handshake-level test before changing the public default again diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index f3fbc0fbd..3c12babce 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -26,8 +26,8 @@ if (osdetector.classifier in ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "o os_suffix = "::" + osdetector.classifier } -def netty_incubator_quic_version = "0.0.72.Final" -def netty_incubator_http3_version = "0.0.29.Final" +def netty_incubator_quic_version = "0.0.73.Final" +def netty_incubator_http3_version = "0.0.30.Final" dependencies { api project(':rsocket-core') diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java index ceff418a2..a6831f5bd 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java @@ -49,7 +49,6 @@ import reactor.netty.Connection; public final class Http3TransportBootstrap { - private Http3TransportBootstrap() {} public static Mono connectClient( From dff7e2ae300e736370820d30a76769726eb29bbd Mon Sep 17 00:00:00 2001 From: "jeroen.veltman" Date: Tue, 31 Mar 2026 15:10:30 +0200 Subject: [PATCH 03/11] Stabilize composite rsocket-java build for broker tests Signed-off-by: jeroen.veltman --- build.gradle | 12 ++++++++---- rsocket-core/build.gradle | 30 ++++++++++++++++++++---------- settings.gradle | 10 +++++++--- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/build.gradle b/build.gradle index 2971a7767..610e8e05d 100644 --- a/build.gradle +++ b/build.gradle @@ -17,12 +17,14 @@ plugins { id 'com.github.sherter.google-java-format' version '0.9' apply false id 'me.champeau.jmh' version '0.7.1' apply false - id 'io.spring.dependency-management' version '1.1.0' apply false + id 'io.spring.dependency-management' version '1.1.7' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false id 'io.github.reyerizo.gradle.jcstress' version '0.8.15' apply false id 'com.github.vlsi.gradle-extensions' version '1.89' apply false } +def compositeConsumer = System.getProperty("rsocket.java.composite.consumer") + boolean isCiServer = ["CI", "CONTINUOUS_INTEGRATION", "TRAVIS", "CIRCLECI", "bamboo_planKey", "GITHUB_ACTION"].with { retainAll(System.getenv().keySet()) return !isEmpty() @@ -226,12 +228,12 @@ subprojects { plugins.withType(JavaLibraryPlugin) { task sourcesJar(type: Jar) { - classifier 'sources' + archiveClassifier.set('sources') from sourceSets.main.allJava } task javadocJar(type: Jar, dependsOn: javadoc) { - classifier 'javadoc' + archiveClassifier.set('javadoc') from javadoc.destinationDir } @@ -249,7 +251,9 @@ subprojects { } } -apply from: "${rootDir}/gradle/publications.gradle" +if (compositeConsumer == null) { + apply from: "${rootDir}/gradle/publications.gradle" +} buildScan { termsOfServiceUrl = 'https://gradle.com/terms-of-service' diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index da5b69b14..efcc89f0a 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -18,9 +18,15 @@ plugins { id 'java-library' id 'maven-publish' id 'signing' - id 'io.morethan.jmhreport' - id 'me.champeau.jmh' - id 'io.github.reyerizo.gradle.jcstress' +} + +def compositeConsumer = System.getProperty("rsocket.java.composite.consumer") +def includePerformancePlugins = compositeConsumer == null + +if (includePerformancePlugins) { + apply plugin: 'io.morethan.jmhreport' + apply plugin: 'me.champeau.jmh' + apply plugin: 'io.github.reyerizo.gradle.jcstress' } dependencies { @@ -40,15 +46,19 @@ dependencies { testRuntimeOnly 'ch.qos.logback:logback-classic' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' - jcstressImplementation(project(":rsocket-test")) - jcstressImplementation 'org.slf4j:slf4j-api' - jcstressImplementation "ch.qos.logback:logback-classic" - jcstressImplementation 'io.projectreactor:reactor-test' + if (includePerformancePlugins) { + jcstressImplementation(project(":rsocket-test")) + jcstressImplementation 'org.slf4j:slf4j-api' + jcstressImplementation "ch.qos.logback:logback-classic" + jcstressImplementation 'io.projectreactor:reactor-test' + } } -jcstress { - mode = 'sanity' //sanity, quick, default, tough - jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.16" +if (includePerformancePlugins) { + jcstress { + mode = 'sanity' //sanity, quick, default, tough + jcstressDependency = "org.openjdk.jcstress:jcstress-core:0.16" + } } jar { diff --git a/settings.gradle b/settings.gradle index 25c3feee5..62b838fe2 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,6 +19,9 @@ plugins { rootProject.name = 'rsocket-java' +def compositeConsumer = System.getProperty("rsocket.java.composite.consumer") +def includeAuxiliaryProjects = compositeConsumer == null + include 'rsocket-core' include 'rsocket-load-balancer' include 'rsocket-micrometer' @@ -27,8 +30,10 @@ include 'rsocket-transport-local' include 'rsocket-transport-netty' include 'rsocket-bom' -include 'rsocket-examples' -include 'benchmarks' +if (includeAuxiliaryProjects) { + include 'rsocket-examples' + include 'benchmarks' +} @@ -38,4 +43,3 @@ gradleEnterprise { termsOfServiceAgree = 'yes' } } - From bb4a8e86bf83b309ddc44399204584f7e6f4040b Mon Sep 17 00:00:00 2001 From: "jeroen.veltman" Date: Tue, 31 Mar 2026 16:14:23 +0200 Subject: [PATCH 04/11] Add repeatable HTTP/3 ping-pong test flow Signed-off-by: jeroen.veltman --- .../QUIC_HTTP3_TRANSPORTS.md | 35 ++++ .../netty/Http3DuplexConnection.java | 59 +++++- .../internal/Http3TransportBootstrap.java | 10 +- .../io/rsocket/transport/netty/Http3Ping.java | 102 ++++++++++ .../transport/netty/Http3PongServer.java | 67 +++++++ scripts/http3-pingpong.sh | 176 ++++++++++++++++++ 6 files changed, 438 insertions(+), 11 deletions(-) create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3Ping.java create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3PongServer.java create mode 100755 scripts/http3-pingpong.sh diff --git a/rsocket-transport-netty/QUIC_HTTP3_TRANSPORTS.md b/rsocket-transport-netty/QUIC_HTTP3_TRANSPORTS.md index 64420c6e6..6477caa57 100644 --- a/rsocket-transport-netty/QUIC_HTTP3_TRANSPORTS.md +++ b/rsocket-transport-netty/QUIC_HTTP3_TRANSPORTS.md @@ -42,3 +42,38 @@ In practice, that means a future `CONNECT`-based RSocket transport likely needs - keep `POST` as the default transport contract until the `CONNECT` path is validated end-to-end - if `CONNECT` is revisited, start with a dedicated experiment around response-header timing and remote settings negotiation - add a focused handshake-level test before changing the public default again + +## Repeatable local checks + +The repository now includes a small helper script for the HTTP/3 ping/pong fixtures: + +- [scripts/http3-pingpong.sh](../scripts/http3-pingpong.sh) + +It creates the temporary Gradle init scripts needed to forward `RSOCKET_*` properties into both the +`Test` workers and the `JavaExec`-based pong server task, which makes the local loop repeatable. + +Examples: + +```bash +./scripts/http3-pingpong.sh integration +./scripts/http3-pingpong.sh pong-server +./scripts/http3-pingpong.sh request-response 1000 +./scripts/http3-pingpong.sh request-stream 100 +./scripts/http3-pingpong.sh all +``` + +The `all` command starts the HTTP/3 pong server in the background, waits for the UDP port to bind, +then runs: + +- `Http3TransportIntegrationTest` +- `Http3Ping.requestResponseTest` +- `Http3Ping.requestStreamTest` + +Useful environment overrides: + +- `RSOCKET_HTTP3_PORT` +- `RSOCKET_HTTP3_BIND_HOST` +- `RSOCKET_HTTP3_CLIENT_HOST` +- `RSOCKET_HTTP3_PATH` +- `RSOCKET_HTTP3_REQUEST_RESPONSE_INTERACTIONS` +- `RSOCKET_HTTP3_REQUEST_STREAM_INTERACTIONS` diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java index 71472cd62..77582bfbc 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java @@ -28,9 +28,9 @@ import io.rsocket.DuplexConnection; import io.rsocket.RSocketErrorException; import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.FrameLengthCodec; import io.rsocket.internal.BaseDuplexConnection; import io.rsocket.internal.UnboundedProcessor; -import java.nio.channels.ClosedChannelException; import java.net.SocketAddress; import java.util.Objects; import reactor.core.publisher.Flux; @@ -41,6 +41,7 @@ public final class Http3DuplexConnection extends BaseDuplexConnection { private final String side; private final Channel connection; private final UnboundedProcessor inbound; + private ByteBuf inboundBuffer; public Http3DuplexConnection(Channel connection) { this("unknown", connection); @@ -50,6 +51,16 @@ public Http3DuplexConnection(String side, Channel connection) { this.connection = Objects.requireNonNull(connection, "connection must not be null"); this.side = side; this.inbound = new UnboundedProcessor(); + this.connection + .closeFuture() + .addListener( + future -> { + if (future.isSuccess()) { + onClose.tryEmitEmpty(); + } else { + onClose.tryEmitError(future.cause()); + } + }); Flux.from(sender) .concatMap( @@ -117,16 +128,50 @@ public Flux receive() { @Override public void sendErrorAndClose(RSocketErrorException e) { final ByteBuf errorFrame = ErrorFrameCodec.encode(alloc(), 0, e); - sender.tryEmitFinal(errorFrame); + sender.tryEmitFinal(FrameLengthCodec.encode(alloc(), errorFrame.readableBytes(), errorFrame)); + } + + @Override + public void sendFrame(int streamId, ByteBuf frame) { + super.sendFrame(streamId, FrameLengthCodec.encode(alloc(), frame.readableBytes(), frame)); } public void handleHeaders(Http3HeadersFrame frame) { } public void handleData(Http3DataFrame frame) { - ByteBuf byteBuf = frame.content().retain(); - frame.release(); - inbound.onNext(byteBuf); + try { + ByteBuf content = frame.content(); + if (!content.isReadable()) { + return; + } + + if (inboundBuffer == null) { + inboundBuffer = alloc().buffer(content.readableBytes()); + } + inboundBuffer.writeBytes(content, content.readerIndex(), content.readableBytes()); + + while (inboundBuffer.readableBytes() >= FrameLengthCodec.FRAME_LENGTH_SIZE) { + inboundBuffer.markReaderIndex(); + int frameLength = FrameLengthCodec.length(inboundBuffer); + if (inboundBuffer.readableBytes() < FrameLengthCodec.FRAME_LENGTH_SIZE + frameLength) { + inboundBuffer.resetReaderIndex(); + break; + } + + inboundBuffer.skipBytes(FrameLengthCodec.FRAME_LENGTH_SIZE); + inbound.onNext(inboundBuffer.readRetainedSlice(frameLength)); + } + + if (inboundBuffer.isReadable()) { + inboundBuffer.discardReadBytes(); + } else { + inboundBuffer.release(); + inboundBuffer = null; + } + } finally { + frame.release(); + } } public void handleError(Throwable cause) { @@ -135,8 +180,8 @@ public void handleError(Throwable cause) { } public void handleInputClosed() { - inbound.onError(new ClosedChannelException()); - onClose.tryEmitEmpty(); + // HTTP/3 request streams can observe half-close on the inbound side while the + // opposite direction is still expected to deliver response frames. } @Override diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java index a6831f5bd..05693ef4a 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java @@ -147,10 +147,11 @@ protected void channelRead( protected void channelInputClosed(ChannelHandlerContext ctx) { if (holder[0] != null) { holder[0].handleInputClosed(); + } else { + established.tryEmitError( + new IllegalStateException( + "HTTP/3 transport closed before handshake completed")); } - established.tryEmitError( - new IllegalStateException( - "HTTP/3 transport closed before handshake completed")); } @Override @@ -363,7 +364,8 @@ protected void channelRead(ChannelHandlerContext ctx, Http3HeadersFrame frame) { response.headers().status("200").add("content-type", config.contentType()); ctx.writeAndFlush(response); established = true; - acceptor.apply(connection).doFinally(__ -> ctx.close()).subscribe(null, t -> ctx.close()); + acceptor.apply(connection).subscribe(null, t -> ctx.close()); + connection.onClose().doFinally(__ -> ctx.close()).subscribe(); return; } diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3Ping.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3Ping.java new file mode 100644 index 000000000..d4611340b --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3Ping.java @@ -0,0 +1,102 @@ +/* + * Copyright 2015-2024 the original author or 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.rsocket.transport.netty; + +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.incubator.codec.http3.Http3; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.test.PerfTest; +import io.rsocket.test.PingClient; +import io.rsocket.transport.netty.client.Http3ClientTransport; +import java.time.Duration; +import org.HdrHistogram.Recorder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +@PerfTest +public final class Http3Ping { + private static final int INTERACTIONS_COUNT = + Integer.valueOf(System.getProperty("RSOCKET_HTTP3_INTERACTIONS", "1000000000")); + private static final String host = System.getProperty("RSOCKET_TEST_HOST", "127.0.0.1"); + private static final int port = + Integer.valueOf( + System.getProperty( + "RSOCKET_TEST_PORT", String.valueOf(Http3TransportConfig.DEFAULT_PORT))); + private static final String path = + System.getProperty("RSOCKET_TEST_PATH", Http3TransportConfig.DEFAULT_PATH); + + @BeforeEach + void setUp() { + System.out.println("Starting ping-pong test (HTTP/3 transport)"); + System.out.println("host: " + host); + System.out.println("port: " + port); + System.out.println("path: " + path); + } + + @Test + void requestResponseTest() throws Exception { + PingClient pingClient = newPingClient(); + Recorder recorder = pingClient.startTracker(Duration.ofSeconds(1)); + + pingClient + .requestResponsePingPong(INTERACTIONS_COUNT, recorder) + .doOnTerminate(() -> System.out.println("Sent " + INTERACTIONS_COUNT + " messages.")) + .blockLast(); + } + + @Test + void requestStreamTest() throws Exception { + PingClient pingClient = newPingClient(); + Recorder recorder = pingClient.startTracker(Duration.ofSeconds(1)); + + pingClient + .requestStreamPingPong(INTERACTIONS_COUNT, recorder) + .doOnTerminate(() -> System.out.println("Sent " + INTERACTIONS_COUNT + " messages.")) + .blockLast(); + } + + private static PingClient newPingClient() throws Exception { + QuicSslContext clientSslContext = + QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(Http3.supportedApplicationProtocols()) + .build(); + + Http3TransportConfig clientConfig = + Http3TransportConfig.builder() + .path(path) + .quicConfig( + QuicTransportConfig.builder() + .sslContext(clientSslContext) + .validateCertificates(false) + .build()) + .build(); + + Mono rSocket = + RSocketConnector.create() + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .keepAlive(Duration.ofMinutes(1), Duration.ofMinutes(30)) + .connect(Http3ClientTransport.create(host, port).config(clientConfig)); + + return new PingClient(rSocket); + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3PongServer.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3PongServer.java new file mode 100644 index 000000000..962f414ba --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3PongServer.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015-2024 the original author or 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.rsocket.transport.netty; + +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.incubator.codec.http3.Http3; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; +import io.rsocket.core.RSocketServer; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.test.PingHandler; +import io.rsocket.transport.netty.server.Http3ServerTransport; + +public final class Http3PongServer { + private static final String bindAddress = System.getProperty("RSOCKET_TEST_HOST", "127.0.0.1"); + private static final int port = + Integer.valueOf( + System.getProperty( + "RSOCKET_TEST_PORT", String.valueOf(Http3TransportConfig.DEFAULT_PORT))); + private static final String path = + System.getProperty("RSOCKET_TEST_PATH", Http3TransportConfig.DEFAULT_PATH); + + public static void main(String... args) throws Exception { + System.out.println("Starting HTTP/3 ping-pong server"); + System.out.println("bind address: " + bindAddress); + System.out.println("port: " + port); + System.out.println("path: " + path); + + SelfSignedCertificate certificate = new SelfSignedCertificate(); + try { + QuicSslContext serverSslContext = + QuicSslContextBuilder.forServer( + certificate.privateKey(), null, certificate.certificate()) + .applicationProtocols(Http3.supportedApplicationProtocols()) + .build(); + + Http3TransportConfig serverConfig = + Http3TransportConfig.builder() + .path(path) + .quicConfig(QuicTransportConfig.builder().sslContext(serverSslContext).build()) + .build(); + + RSocketServer.create(new PingHandler()) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(Http3ServerTransport.create(bindAddress, port).config(serverConfig)) + .block() + .onClose() + .block(); + } finally { + certificate.delete(); + } + } +} diff --git a/scripts/http3-pingpong.sh b/scripts/http3-pingpong.sh new file mode 100755 index 000000000..7bc40b865 --- /dev/null +++ b/scripts/http3-pingpong.sh @@ -0,0 +1,176 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$ROOT_DIR" + +GRADLEW="$ROOT_DIR/gradlew" +TEST_CLASS_REQUEST_RESPONSE="io.rsocket.transport.netty.Http3Ping.requestResponseTest" +TEST_CLASS_REQUEST_STREAM="io.rsocket.transport.netty.Http3Ping.requestStreamTest" + +PORT="${RSOCKET_HTTP3_PORT:-7878}" +SERVER_BIND_HOST="${RSOCKET_HTTP3_BIND_HOST:-0.0.0.0}" +CLIENT_HOST="${RSOCKET_HTTP3_CLIENT_HOST:-127.0.0.1}" +ROUTE_PATH="${RSOCKET_HTTP3_PATH:-/rsocket}" +REQUEST_RESPONSE_INTERACTIONS="${RSOCKET_HTTP3_REQUEST_RESPONSE_INTERACTIONS:-1000}" +REQUEST_STREAM_INTERACTIONS="${RSOCKET_HTTP3_REQUEST_STREAM_INTERACTIONS:-100}" + +JAVA_BIN="${JAVA_HOME:-}" +if [[ -z "$JAVA_BIN" ]] && command -v /usr/libexec/java_home >/dev/null 2>&1; then + JAVA_BIN="$("/usr/libexec/java_home" -v 13)" +fi +if [[ -n "$JAVA_BIN" ]]; then + export JAVA_HOME="$JAVA_BIN" +fi + +TMP_DIR="$(mktemp -d)" +cleanup() { + rm -rf "$TMP_DIR" +} +trap cleanup EXIT + +cat >"$TMP_DIR/http3-javaexec-init.gradle" <<'EOF' +allprojects { + afterEvaluate { project -> + if (project.path == ':rsocket-transport-netty') { + project.tasks.register('runHttp3PongServer', JavaExec) { + classpath = project.sourceSets.test.runtimeClasspath + mainClass = 'io.rsocket.transport.netty.Http3PongServer' + systemProperties System.properties.findAll { key, value -> key.startsWith('RSOCKET_') } + } + } + } +} +EOF + +cat >"$TMP_DIR/http3-test-props-init.gradle" <<'EOF' +allprojects { + tasks.withType(Test).configureEach { + System.properties.findAll { key, value -> key.startsWith('RSOCKET_') || key == 'TEST_PERF_ENABLED' } + .each { key, value -> + systemProperty key.toString(), value + } + } +} +EOF + +gradle_base() { + "$GRADLEW" --no-daemon --console=plain "$@" +} + +wait_for_server() { + for _ in $(seq 1 60); do + if lsof -nP -iUDP:"$PORT" >/dev/null 2>&1; then + return 0 + fi + sleep 1 + done + echo "HTTP/3 pong server did not bind to UDP port $PORT in time" >&2 + return 1 +} + +start_server_background() { + gradle_base \ + -DRSOCKET_TEST_HOST="$SERVER_BIND_HOST" \ + -DRSOCKET_TEST_PORT="$PORT" \ + -DRSOCKET_TEST_PATH="$ROUTE_PATH" \ + -I "$TMP_DIR/http3-javaexec-init.gradle" \ + :rsocket-transport-netty:runHttp3PongServer & + SERVER_PID=$! + trap 'stop_server; cleanup' EXIT + wait_for_server +} + +stop_server() { + if [[ -n "${SERVER_PID:-}" ]]; then + kill "$SERVER_PID" >/dev/null 2>&1 || true + wait "$SERVER_PID" >/dev/null 2>&1 || true + unset SERVER_PID + fi +} + +run_request_response_test() { + TEST_PERF_ENABLED=true \ + gradle_base \ + --rerun-tasks \ + -I "$TMP_DIR/http3-test-props-init.gradle" \ + -DRSOCKET_TEST_HOST="$CLIENT_HOST" \ + -DRSOCKET_TEST_PORT="$PORT" \ + -DRSOCKET_TEST_PATH="$ROUTE_PATH" \ + -DRSOCKET_HTTP3_INTERACTIONS="${1:-$REQUEST_RESPONSE_INTERACTIONS}" \ + :rsocket-transport-netty:test \ + --tests "$TEST_CLASS_REQUEST_RESPONSE" +} + +run_request_stream_test() { + TEST_PERF_ENABLED=true \ + gradle_base \ + --rerun-tasks \ + -I "$TMP_DIR/http3-test-props-init.gradle" \ + -DRSOCKET_TEST_HOST="$CLIENT_HOST" \ + -DRSOCKET_TEST_PORT="$PORT" \ + -DRSOCKET_TEST_PATH="$ROUTE_PATH" \ + -DRSOCKET_HTTP3_INTERACTIONS="${1:-$REQUEST_STREAM_INTERACTIONS}" \ + :rsocket-transport-netty:test \ + --tests "$TEST_CLASS_REQUEST_STREAM" +} + +run_integration_test() { + gradle_base \ + --rerun-tasks \ + :rsocket-transport-netty:test \ + --tests 'io.rsocket.transport.netty.Http3TransportIntegrationTest' +} + +usage() { + cat < [interactions] + +Commands: + pong-server Run Http3PongServer in the foreground + request-response Run Http3Ping.requestResponseTest against an existing pong server + request-stream Run Http3Ping.requestStreamTest against an existing pong server + integration Run Http3TransportIntegrationTest + all Start pong server in the background and run integration, request-response, and request-stream + +Environment overrides: + RSOCKET_HTTP3_PORT UDP port to use (default: $PORT) + RSOCKET_HTTP3_BIND_HOST Server bind host (default: $SERVER_BIND_HOST) + RSOCKET_HTTP3_CLIENT_HOST Client target host (default: $CLIENT_HOST) + RSOCKET_HTTP3_PATH HTTP/3 route path (default: $ROUTE_PATH) + RSOCKET_HTTP3_REQUEST_RESPONSE_INTERACTIONS Default request-response count (default: $REQUEST_RESPONSE_INTERACTIONS) + RSOCKET_HTTP3_REQUEST_STREAM_INTERACTIONS Default request-stream count (default: $REQUEST_STREAM_INTERACTIONS) +EOF +} + +COMMAND="${1:-}" +case "$COMMAND" in + pong-server) + exec gradle_base \ + -DRSOCKET_TEST_HOST="$SERVER_BIND_HOST" \ + -DRSOCKET_TEST_PORT="$PORT" \ + -DRSOCKET_TEST_PATH="$ROUTE_PATH" \ + -I "$TMP_DIR/http3-javaexec-init.gradle" \ + :rsocket-transport-netty:runHttp3PongServer + ;; + request-response) + run_request_response_test "${2:-$REQUEST_RESPONSE_INTERACTIONS}" + ;; + request-stream) + run_request_stream_test "${2:-$REQUEST_STREAM_INTERACTIONS}" + ;; + integration) + run_integration_test + ;; + all) + start_server_background + run_integration_test + run_request_response_test "$REQUEST_RESPONSE_INTERACTIONS" + run_request_stream_test "$REQUEST_STREAM_INTERACTIONS" + ;; + *) + usage + exit 1 + ;; +esac From 758fcf777412c4dc97fefca11f3de6941a908f48 Mon Sep 17 00:00:00 2001 From: "jeroen.veltman" Date: Wed, 1 Apr 2026 07:27:51 +0200 Subject: [PATCH 05/11] Split QUIC and HTTP/3 transports into dedicated artifacts Signed-off-by: jeroen.veltman --- README.md | 9 + .../rsocket/internal/UnboundedProcessor.java | 164 ++++++++----- .../resume/ResumableDuplexConnection.java | 4 +- .../QUIC_HTTP3_TRANSPORTS.md | 29 ++- rsocket-transport-h3/build.gradle | 55 +++++ .../netty/Http3DuplexConnection.java | 20 +- .../transport/netty/Http3TransportConfig.java | 0 .../netty/client/Http3ClientTransport.java | 0 .../internal/Http3TransportBootstrap.java | 0 .../netty/server/Http3ServerTransport.java | 0 .../io/rsocket/transport/netty/Http3Ping.java | 0 .../netty/Http3PingPongIntegrationTest.java | 227 ++++++++++++++++++ .../transport/netty/Http3PongServer.java | 0 .../netty/Http3TransportIntegrationTest.java | 0 .../client/Http3ClientTransportTest.java | 0 rsocket-transport-netty/build.gradle | 17 +- .../client/WebsocketClientTransport.java | 9 +- rsocket-transport-quic/build.gradle | 65 +++++ .../transport/netty/QuicDuplexConnection.java | 14 +- .../transport/netty/QuicTransportConfig.java | 0 .../netty/client/QuicClientTransport.java | 0 .../internal/QuicTransportBootstrap.java | 0 .../netty/server/QuicServerTransport.java | 0 .../netty/QuicTransportIntegrationTest.java | 0 .../netty/client/QuicClientTransportTest.java | 0 scripts/http3-pingpong.sh | 38 ++- settings.gradle | 2 + 27 files changed, 551 insertions(+), 102 deletions(-) rename {rsocket-transport-netty => rsocket-transport-h3}/QUIC_HTTP3_TRANSPORTS.md (78%) create mode 100644 rsocket-transport-h3/build.gradle rename {rsocket-transport-netty => rsocket-transport-h3}/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java (92%) rename {rsocket-transport-netty => rsocket-transport-h3}/src/main/java/io/rsocket/transport/netty/Http3TransportConfig.java (100%) rename {rsocket-transport-netty => rsocket-transport-h3}/src/main/java/io/rsocket/transport/netty/client/Http3ClientTransport.java (100%) rename {rsocket-transport-netty => rsocket-transport-h3}/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java (100%) rename {rsocket-transport-netty => rsocket-transport-h3}/src/main/java/io/rsocket/transport/netty/server/Http3ServerTransport.java (100%) rename {rsocket-transport-netty => rsocket-transport-h3}/src/test/java/io/rsocket/transport/netty/Http3Ping.java (100%) create mode 100644 rsocket-transport-h3/src/test/java/io/rsocket/transport/netty/Http3PingPongIntegrationTest.java rename {rsocket-transport-netty => rsocket-transport-h3}/src/test/java/io/rsocket/transport/netty/Http3PongServer.java (100%) rename {rsocket-transport-netty => rsocket-transport-h3}/src/test/java/io/rsocket/transport/netty/Http3TransportIntegrationTest.java (100%) rename {rsocket-transport-netty => rsocket-transport-h3}/src/test/java/io/rsocket/transport/netty/client/Http3ClientTransportTest.java (100%) create mode 100644 rsocket-transport-quic/build.gradle rename {rsocket-transport-netty => rsocket-transport-quic}/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java (92%) rename {rsocket-transport-netty => rsocket-transport-quic}/src/main/java/io/rsocket/transport/netty/QuicTransportConfig.java (100%) rename {rsocket-transport-netty => rsocket-transport-quic}/src/main/java/io/rsocket/transport/netty/client/QuicClientTransport.java (100%) rename {rsocket-transport-netty => rsocket-transport-quic}/src/main/java/io/rsocket/transport/netty/internal/QuicTransportBootstrap.java (100%) rename {rsocket-transport-netty => rsocket-transport-quic}/src/main/java/io/rsocket/transport/netty/server/QuicServerTransport.java (100%) rename {rsocket-transport-netty => rsocket-transport-quic}/src/test/java/io/rsocket/transport/netty/QuicTransportIntegrationTest.java (100%) rename {rsocket-transport-netty => rsocket-transport-quic}/src/test/java/io/rsocket/transport/netty/client/QuicClientTransportTest.java (100%) diff --git a/README.md b/README.md index 7ed3244b8..13776a014 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,16 @@ repositories { dependencies { implementation 'io.rsocket:rsocket-core:1.2.0-SNAPSHOT' implementation 'io.rsocket:rsocket-transport-netty:1.2.0-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-quic:1.2.0-SNAPSHOT' // optional QUIC transport + implementation 'io.rsocket:rsocket-transport-h3:1.2.0-SNAPSHOT' // optional HTTP/3 transport } ``` +For a minimal HTTP/3 `"PING" -> "PONG"` example, see +[Http3PingPongIntegrationTest.java](rsocket-transport-h3/src/test/java/io/rsocket/transport/netty/Http3PingPongIntegrationTest.java) +and the transport notes in +[QUIC_HTTP3_TRANSPORTS.md](rsocket-transport-h3/QUIC_HTTP3_TRANSPORTS.md). + Snapshots are available via [oss.jfrog.org](oss.jfrog.org) (OJO). Example: @@ -46,6 +53,8 @@ repositories { dependencies { implementation 'io.rsocket:rsocket-core:1.2.0-SNAPSHOT' implementation 'io.rsocket:rsocket-transport-netty:1.2.0-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-quic:1.2.0-SNAPSHOT' // optional QUIC transport + implementation 'io.rsocket:rsocket-transport-h3:1.2.0-SNAPSHOT' // optional HTTP/3 transport } ``` diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java index c96a7aed2..730ea3bcb 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -18,6 +18,7 @@ import io.netty.buffer.ByteBuf; import io.rsocket.internal.jctools.queues.MpscUnboundedArrayQueue; +import java.lang.reflect.Method; import java.util.Objects; import java.util.Queue; import java.util.concurrent.CancellationException; @@ -97,6 +98,8 @@ public final class UnboundedProcessor extends Flux boolean outputFused; + @Nullable private static final Method THREAD_ID_METHOD = resolveThreadIdMethod(); + public UnboundedProcessor() { this(() -> {}); } @@ -141,7 +144,7 @@ public boolean tryEmitPrioritized(ByteBuf t) { } if (!this.priorityQueue.offer(t)) { - onError(Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext())); + tryEmitError(Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext())); release(t); return false; } @@ -177,7 +180,7 @@ public boolean tryEmitNormal(ByteBuf t) { } if (!this.queue.offer(t)) { - onError(Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext())); + tryEmitError(Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext())); release(t); return false; } @@ -207,26 +210,30 @@ public boolean tryEmitNormal(ByteBuf t) { return true; } - public boolean tryEmitFinal(ByteBuf t) { + public boolean tryEmitError(Throwable t) { + Objects.requireNonNull(t, "throwable must not be null"); + if (this.done || this.cancelled) { - release(t); + Operators.onErrorDropped(t, currentContext()); return false; } - this.last = t; + this.error = t; this.done = true; - final long previousState = markValueAddedAndTerminated(this); - if (isFinalized(previousState)) { - this.clearSafely(); + final long previousState = markTerminatedOrFinalized(this); + if (isFinalized(previousState) + || isDisposed(previousState) + || isCancelled(previousState) + || isTerminated(previousState)) { + Operators.onErrorDropped(t, currentContext()); return false; } if (isSubscriberReady(previousState)) { if (this.outputFused) { - // fast path for fusion - this.actual.onNext(null); - this.actual.onComplete(); + // fast path for fusion scenario + this.actual.onError(t); return true; } @@ -234,32 +241,23 @@ public boolean tryEmitFinal(ByteBuf t) { return true; } - drainRegular((previousState | FLAG_TERMINATED | FLAG_HAS_VALUE) + 1); + if (!hasValue(previousState)) { + // fast path no-values scenario + this.actual.onError(t); + return true; + } + + drainRegular((previousState | FLAG_TERMINATED) + 1); } return true; } - @Deprecated - public void onNextPrioritized(ByteBuf t) { - tryEmitPrioritized(t); - } - - @Override - @Deprecated - public void onNext(ByteBuf t) { - tryEmitNormal(t); - } - - @Override - @Deprecated - public void onError(Throwable t) { + public boolean tryEmitComplete() { if (this.done || this.cancelled) { - Operators.onErrorDropped(t, currentContext()); - return; + return false; } - this.error = t; this.done = true; final long previousState = markTerminatedOrFinalized(this); @@ -267,66 +265,85 @@ public void onError(Throwable t) { || isDisposed(previousState) || isCancelled(previousState) || isTerminated(previousState)) { - Operators.onErrorDropped(t, currentContext()); - return; + return false; } if (isSubscriberReady(previousState)) { if (this.outputFused) { // fast path for fusion scenario - this.actual.onError(t); - return; + this.actual.onComplete(); + return true; } if (isWorkInProgress(previousState)) { - return; + return true; } if (!hasValue(previousState)) { - // fast path no-values scenario - this.actual.onError(t); - return; + this.actual.onComplete(); + return true; } drainRegular((previousState | FLAG_TERMINATED) + 1); } + + return true; } - @Override - @Deprecated - public void onComplete() { + public boolean tryEmitFinal(ByteBuf t) { if (this.done || this.cancelled) { - return; + release(t); + return false; } + this.last = t; this.done = true; - final long previousState = markTerminatedOrFinalized(this); - if (isFinalized(previousState) - || isDisposed(previousState) - || isCancelled(previousState) - || isTerminated(previousState)) { - return; + final long previousState = markValueAddedAndTerminated(this); + if (isFinalized(previousState)) { + this.clearSafely(); + return false; } if (isSubscriberReady(previousState)) { if (this.outputFused) { - // fast path for fusion scenario + // fast path for fusion + this.actual.onNext(null); this.actual.onComplete(); - return; + return true; } if (isWorkInProgress(previousState)) { - return; - } - - if (!hasValue(previousState)) { - this.actual.onComplete(); - return; + return true; } - drainRegular((previousState | FLAG_TERMINATED) + 1); + drainRegular((previousState | FLAG_TERMINATED | FLAG_HAS_VALUE) + 1); } + + return true; + } + + @Deprecated + public void onNextPrioritized(ByteBuf t) { + tryEmitPrioritized(t); + } + + @Override + @Deprecated + public void onNext(ByteBuf t) { + tryEmitNormal(t); + } + + @Override + @Deprecated + public void onError(Throwable t) { + tryEmitError(t); + } + + @Override + @Deprecated + public void onComplete() { + tryEmitComplete(); } void drainRegular(long expectedState) { @@ -1091,7 +1108,7 @@ static void log( instance, action, action, - Thread.currentThread().getId(), + currentThreadId(), formatState(initialState, 64), formatState(committedState, 64)), new RuntimeException()); @@ -1101,7 +1118,7 @@ static void log( "[%s][%s][%s][%s-%s]", instance, action, - Thread.currentThread().getId(), + currentThreadId(), formatState(initialState, 64), formatState(committedState, 64))); } @@ -1130,7 +1147,7 @@ static void log( instance, action, action, - Thread.currentThread().getId(), + currentThreadId(), formatState(initialState, 32), formatState(committedState, 32)), new RuntimeException()); @@ -1140,12 +1157,39 @@ static void log( "[%s][%s][%s][%s-%s]", instance, action, - Thread.currentThread().getId(), + currentThreadId(), formatState(initialState, 32), formatState(committedState, 32))); } } + private static long currentThreadId() { + Thread currentThread = Thread.currentThread(); + if (THREAD_ID_METHOD != null) { + try { + return (long) THREAD_ID_METHOD.invoke(currentThread); + } catch (Throwable ignore) { + // Fallback to the legacy method below for older runtimes or reflective failures. + } + } + + return legacyThreadId(currentThread); + } + + @Nullable + private static Method resolveThreadIdMethod() { + try { + return Thread.class.getMethod("threadId"); + } catch (NoSuchMethodException e) { + return null; + } + } + + @SuppressWarnings("deprecation") + private static long legacyThreadId(Thread thread) { + return thread.getId(); + } + static String formatState(long state, int size) { final String defaultFormat = Long.toBinaryString(state); final StringBuilder formatted = new StringBuilder(); diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java index c8811b9b3..fe949a62a 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -241,7 +241,7 @@ public void dispose() { if (activeConnection == DisposedConnection.INSTANCE) { return; } - savableFramesSender.onComplete(); + savableFramesSender.tryEmitComplete(); activeConnection .onClose() .subscribe( @@ -262,7 +262,7 @@ void dispose(DuplexConnection nextConnection, @Nullable Throwable e) { if (activeConnection == DisposedConnection.INSTANCE) { return; } - savableFramesSender.onComplete(); + savableFramesSender.tryEmitComplete(); nextConnection .onClose() .subscribe( diff --git a/rsocket-transport-netty/QUIC_HTTP3_TRANSPORTS.md b/rsocket-transport-h3/QUIC_HTTP3_TRANSPORTS.md similarity index 78% rename from rsocket-transport-netty/QUIC_HTTP3_TRANSPORTS.md rename to rsocket-transport-h3/QUIC_HTTP3_TRANSPORTS.md index 6477caa57..18e732695 100644 --- a/rsocket-transport-netty/QUIC_HTTP3_TRANSPORTS.md +++ b/rsocket-transport-h3/QUIC_HTTP3_TRANSPORTS.md @@ -1,10 +1,11 @@ # QUIC and HTTP/3 transport notes -This module now contains prototype Netty transports for: +This module contains the prototype Netty HTTP/3 transport for: -- `quic://` via `QuicClientTransport` and `QuicServerTransport` - `h3://` via `Http3ClientTransport` and `Http3ServerTransport` +The shared QUIC transport now lives in the sibling `rsocket-transport-quic` artifact. + ## Current HTTP/3 transport contract The current `h3://` transport uses a long-lived HTTP/3 `POST` request stream: @@ -57,6 +58,7 @@ Examples: ```bash ./scripts/http3-pingpong.sh integration ./scripts/http3-pingpong.sh pong-server +./scripts/http3-pingpong.sh ping-pong ./scripts/http3-pingpong.sh request-response 1000 ./scripts/http3-pingpong.sh request-stream 100 ./scripts/http3-pingpong.sh all @@ -65,6 +67,7 @@ Examples: The `all` command starts the HTTP/3 pong server in the background, waits for the UDP port to bind, then runs: +- `Http3PingPongIntegrationTest` - `Http3TransportIntegrationTest` - `Http3Ping.requestResponseTest` - `Http3Ping.requestStreamTest` @@ -77,3 +80,25 @@ Useful environment overrides: - `RSOCKET_HTTP3_PATH` - `RSOCKET_HTTP3_REQUEST_RESPONSE_INTERACTIONS` - `RSOCKET_HTTP3_REQUEST_STREAM_INTERACTIONS` + +## Minimal ping-pong example + +For the clearest end-to-end HTTP/3 example in the repository, see: + +- [Http3PingPongIntegrationTest.java](src/test/java/io/rsocket/transport/netty/Http3PingPongIntegrationTest.java) + +That test keeps the scenario intentionally small: + +- starts an `Http3ServerTransport` +- sends `"PING"` from the client with `requestResponse` +- asserts the server receives `"PING"` +- returns `"PONG"` +- asserts the client receives `"PONG"` + +You can run just that test with: + +```bash +JAVA_HOME=$(/usr/libexec/java_home -v 13) ./gradlew --no-daemon --console=plain \ + :rsocket-transport-h3:test \ + --tests 'io.rsocket.transport.netty.Http3PingPongIntegrationTest' +``` diff --git a/rsocket-transport-h3/build.gradle b/rsocket-transport-h3/build.gradle new file mode 100644 index 000000000..f7d896ee5 --- /dev/null +++ b/rsocket-transport-h3/build.gradle @@ -0,0 +1,55 @@ +/* + * Copyright 2015-2026 the original author or 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. + */ + +plugins { + id 'java-library' + id 'maven-publish' + id 'signing' +} + +def netty_incubator_http3_version = "0.0.30.Final" + +dependencies { + api project(':rsocket-transport-netty') + api project(':rsocket-transport-quic') + + implementation "io.netty.incubator:netty-incubator-codec-http3:${netty_incubator_http3_version}" + + testImplementation project(':rsocket-test') + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.mockito:mockito-core' + testImplementation 'org.mockito:mockito-junit-jupiter' + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.junit.jupiter:junit-jupiter-params' + + testRuntimeOnly 'org.bouncycastle:bcpkix-jdk15on' + testRuntimeOnly 'ch.qos.logback:logback-classic' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' +} + +jar { + manifest { + attributes("Automatic-Module-Name": "rsocket.transport.h3") + } +} + +test { + minHeapSize = "512m" + maxHeapSize = "4096m" +} + +description = 'HTTP/3 RSocket transport implementations built on Reactor Netty and QUIC' diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java b/rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java similarity index 92% rename from rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java rename to rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java index 77582bfbc..eb9861e96 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java +++ b/rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java @@ -55,6 +55,7 @@ public Http3DuplexConnection(String side, Channel connection) { .closeFuture() .addListener( future -> { + releaseInboundBuffer(); if (future.isSuccess()) { onClose.tryEmitEmpty(); } else { @@ -80,7 +81,8 @@ public Http3DuplexConnection(String side, Channel connection) { 1) .doOnError( throwable -> { - inbound.onError(throwable); + releaseInboundBuffer(); + inbound.tryEmitError(throwable); onClose.tryEmitError(throwable); this.connection.close(); }) @@ -112,6 +114,7 @@ public SocketAddress remoteAddress() { @Override protected void doOnClose() { + releaseInboundBuffer(); connection.close(); } @@ -160,14 +163,13 @@ public void handleData(Http3DataFrame frame) { } inboundBuffer.skipBytes(FrameLengthCodec.FRAME_LENGTH_SIZE); - inbound.onNext(inboundBuffer.readRetainedSlice(frameLength)); + inbound.tryEmitNormal(inboundBuffer.readRetainedSlice(frameLength)); } if (inboundBuffer.isReadable()) { inboundBuffer.discardReadBytes(); } else { - inboundBuffer.release(); - inboundBuffer = null; + releaseInboundBuffer(); } } finally { frame.release(); @@ -175,7 +177,8 @@ public void handleData(Http3DataFrame frame) { } public void handleError(Throwable cause) { - inbound.onError(cause); + releaseInboundBuffer(); + inbound.tryEmitError(cause); onClose.tryEmitError(cause); } @@ -184,6 +187,13 @@ public void handleInputClosed() { // opposite direction is still expected to deliver response frames. } + private void releaseInboundBuffer() { + if (inboundBuffer != null) { + inboundBuffer.release(); + inboundBuffer = null; + } + } + @Override public String toString() { return "Http3DuplexConnection{" + "side='" + side + '\'' + ", connection=" + connection + '}'; diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3TransportConfig.java b/rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/Http3TransportConfig.java similarity index 100% rename from rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/Http3TransportConfig.java rename to rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/Http3TransportConfig.java diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/Http3ClientTransport.java b/rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/client/Http3ClientTransport.java similarity index 100% rename from rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/Http3ClientTransport.java rename to rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/client/Http3ClientTransport.java diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java b/rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java similarity index 100% rename from rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java rename to rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/internal/Http3TransportBootstrap.java diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/Http3ServerTransport.java b/rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/server/Http3ServerTransport.java similarity index 100% rename from rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/Http3ServerTransport.java rename to rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/server/Http3ServerTransport.java diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3Ping.java b/rsocket-transport-h3/src/test/java/io/rsocket/transport/netty/Http3Ping.java similarity index 100% rename from rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3Ping.java rename to rsocket-transport-h3/src/test/java/io/rsocket/transport/netty/Http3Ping.java diff --git a/rsocket-transport-h3/src/test/java/io/rsocket/transport/netty/Http3PingPongIntegrationTest.java b/rsocket-transport-h3/src/test/java/io/rsocket/transport/netty/Http3PingPongIntegrationTest.java new file mode 100644 index 000000000..e1b506cba --- /dev/null +++ b/rsocket-transport-h3/src/test/java/io/rsocket/transport/netty/Http3PingPongIntegrationTest.java @@ -0,0 +1,227 @@ +/* + * Copyright 2015-2026 the original author or 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.rsocket.transport.netty; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.incubator.codec.http3.Http3; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.transport.netty.client.Http3ClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.Http3ServerTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.time.Duration; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +class Http3PingPongIntegrationTest { + + @Test + void shouldReturnPongForPing() throws Exception { + try (TestContext context = + TestContext.create( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + assertPing(payload); + return Mono.just(DefaultPayload.create("PONG")); + } + })) { + String response = + context.client + .requestResponse(DefaultPayload.create("PING")) + .map(Http3PingPongIntegrationTest::releaseToString) + .block(Duration.ofSeconds(10)); + + assertThat(response).isEqualTo("PONG"); + } + } + + @Test + void shouldReturnPongsForRequestStream() throws Exception { + try (TestContext context = + TestContext.create( + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + assertPing(payload); + return Flux.just( + DefaultPayload.create("PONG"), + DefaultPayload.create("PONG"), + DefaultPayload.create("PONG")); + } + })) { + List responses = + context.client + .requestStream(DefaultPayload.create("PING")) + .map(Http3PingPongIntegrationTest::releaseToString) + .collectList() + .block(Duration.ofSeconds(10)); + + assertThat(responses).containsExactly("PONG", "PONG", "PONG"); + } + } + + @Test + void shouldDeliverPingForFireAndForget() throws Exception { + CountDownLatch received = new CountDownLatch(1); + + try (TestContext context = + TestContext.create( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + assertPing(payload); + received.countDown(); + return Mono.empty(); + } + })) { + context.client.fireAndForget(DefaultPayload.create("PING")).block(Duration.ofSeconds(10)); + + assertThat(received.await(10, TimeUnit.SECONDS)).isTrue(); + } + } + + @Test + void shouldReturnPongsForRequestChannel() throws Exception { + try (TestContext context = + TestContext.create( + new RSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .map( + payload -> { + assertPing(payload); + return DefaultPayload.create("PONG"); + }); + } + })) { + List responses = + context.client + .requestChannel( + Flux.just( + DefaultPayload.create("PING"), + DefaultPayload.create("PING"), + DefaultPayload.create("PING"))) + .map(Http3PingPongIntegrationTest::releaseToString) + .collectList() + .block(Duration.ofSeconds(10)); + + assertThat(responses).containsExactlyElementsOf(Arrays.asList("PONG", "PONG", "PONG")); + } + } + + private static void assertPing(Payload payload) { + try { + assertThat(payload.getDataUtf8()).isEqualTo("PING"); + } finally { + payload.release(); + } + } + + private static String releaseToString(Payload payload) { + try { + return payload.getDataUtf8(); + } finally { + payload.release(); + } + } + + private static final class TestContext implements AutoCloseable { + + private final SelfSignedCertificate certificate; + private final CloseableChannel server; + private final io.rsocket.RSocket client; + + private TestContext( + SelfSignedCertificate certificate, CloseableChannel server, io.rsocket.RSocket client) { + this.certificate = certificate; + this.server = server; + this.client = client; + } + + static TestContext create(RSocket responder) throws Exception { + SelfSignedCertificate certificate = new SelfSignedCertificate(); + QuicSslContext serverSslContext = + QuicSslContextBuilder.forServer( + certificate.privateKey(), null, certificate.certificate()) + .applicationProtocols(Http3.supportedApplicationProtocols()) + .build(); + QuicSslContext clientSslContext = + QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(Http3.supportedApplicationProtocols()) + .build(); + + Http3TransportConfig serverConfig = + Http3TransportConfig.builder() + .path("/rsocket") + .quicConfig(QuicTransportConfig.builder().sslContext(serverSslContext).build()) + .build(); + Http3TransportConfig clientConfig = + Http3TransportConfig.builder() + .path("/rsocket") + .quicConfig( + QuicTransportConfig.builder() + .sslContext(clientSslContext) + .validateCertificates(false) + .build()) + .build(); + + CloseableChannel server = + RSocketServer.create((setup, sendingSocket) -> Mono.just(responder)) + .bind(Http3ServerTransport.create(0).config(serverConfig)) + .block(Duration.ofSeconds(10)); + + io.rsocket.RSocket client = + RSocketConnector.connectWith( + Http3ClientTransport.create("127.0.0.1", server.address().getPort()) + .config(clientConfig)) + .block(Duration.ofSeconds(10)); + + return new TestContext(certificate, server, client); + } + + @Override + public void close() throws Exception { + try { + client.dispose(); + } finally { + try { + server.dispose(); + } finally { + certificate.delete(); + } + } + } + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3PongServer.java b/rsocket-transport-h3/src/test/java/io/rsocket/transport/netty/Http3PongServer.java similarity index 100% rename from rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3PongServer.java rename to rsocket-transport-h3/src/test/java/io/rsocket/transport/netty/Http3PongServer.java diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3TransportIntegrationTest.java b/rsocket-transport-h3/src/test/java/io/rsocket/transport/netty/Http3TransportIntegrationTest.java similarity index 100% rename from rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/Http3TransportIntegrationTest.java rename to rsocket-transport-h3/src/test/java/io/rsocket/transport/netty/Http3TransportIntegrationTest.java diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/Http3ClientTransportTest.java b/rsocket-transport-h3/src/test/java/io/rsocket/transport/netty/client/Http3ClientTransportTest.java similarity index 100% rename from rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/Http3ClientTransportTest.java rename to rsocket-transport-h3/src/test/java/io/rsocket/transport/netty/client/Http3ClientTransportTest.java diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 3c12babce..606deeb4b 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -18,27 +18,12 @@ plugins { id 'java-library' id 'maven-publish' id 'signing' - id "com.google.osdetector" version "1.4.0" } -def os_suffix = "" -if (osdetector.classifier in ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "osx-aarch_64", "windows-x86_64"]) { - os_suffix = "::" + osdetector.classifier -} - -def netty_incubator_quic_version = "0.0.73.Final" -def netty_incubator_http3_version = "0.0.30.Final" - dependencies { api project(':rsocket-core') api "io.projectreactor.netty:reactor-netty-core" api "io.projectreactor.netty:reactor-netty-http" - implementation "io.netty.incubator:netty-incubator-codec-classes-quic:${netty_incubator_quic_version}" - implementation "io.netty.incubator:netty-incubator-codec-http3:${netty_incubator_http3_version}" - runtimeOnly "io.netty.incubator:netty-incubator-codec-native-quic:${netty_incubator_quic_version}" - if (os_suffix) { - runtimeOnly(group: "io.netty.incubator", name: "netty-incubator-codec-native-quic", version: netty_incubator_quic_version, classifier: osdetector.classifier) - } api 'org.slf4j:slf4j-api' testImplementation project(':rsocket-test') @@ -52,7 +37,7 @@ dependencies { testRuntimeOnly 'org.bouncycastle:bcpkix-jdk15on' testRuntimeOnly 'ch.qos.logback:logback-classic' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' - testRuntimeOnly 'io.netty:netty-tcnative-boringssl-static' + os_suffix + testRuntimeOnly 'io.netty:netty-tcnative-boringssl-static' } jar { diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/WebsocketClientTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/WebsocketClientTransport.java index 86be47893..4e9ff22d9 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/WebsocketClientTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/WebsocketClientTransport.java @@ -100,7 +100,7 @@ public static WebsocketClientTransport create(InetSocketAddress address) { * @throws NullPointerException if {@code client} or {@code path} is {@code null} */ public static WebsocketClientTransport create(TcpClient client) { - return new WebsocketClientTransport(HttpClient.from(client), DEFAULT_PATH); + return new WebsocketClientTransport(httpClientFrom(client), DEFAULT_PATH); } /** @@ -117,7 +117,7 @@ public static WebsocketClientTransport create(URI uri) { (isSecure ? TcpClient.create().secure() : TcpClient.create()) .host(uri.getHost()) .port(uri.getPort() == -1 ? (isSecure ? 443 : 80) : uri.getPort()); - return new WebsocketClientTransport(HttpClient.from(client), uri.getPath()); + return new WebsocketClientTransport(httpClientFrom(client), uri.getPath()); } /** @@ -132,6 +132,11 @@ public static WebsocketClientTransport create(HttpClient client, String path) { return new WebsocketClientTransport(client, path); } + @SuppressWarnings("deprecation") + private static HttpClient httpClientFrom(TcpClient client) { + return HttpClient.from(client); + } + /** * Add a header and value(s) to use for the WebSocket handshake request. * diff --git a/rsocket-transport-quic/build.gradle b/rsocket-transport-quic/build.gradle new file mode 100644 index 000000000..6c71db79b --- /dev/null +++ b/rsocket-transport-quic/build.gradle @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2026 the original author or 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. + */ + +plugins { + id 'java-library' + id 'maven-publish' + id 'signing' + id "com.google.osdetector" version "1.4.0" +} + +def os_suffix = "" +if (osdetector.classifier in ["linux-x86_64", "linux-aarch_64", "osx-x86_64", "osx-aarch_64", "windows-x86_64"]) { + os_suffix = "::" + osdetector.classifier +} + +def netty_incubator_quic_version = "0.0.73.Final" + +dependencies { + api project(':rsocket-transport-netty') + + implementation "io.netty.incubator:netty-incubator-codec-classes-quic:${netty_incubator_quic_version}" + runtimeOnly "io.netty.incubator:netty-incubator-codec-native-quic:${netty_incubator_quic_version}" + if (os_suffix) { + runtimeOnly(group: "io.netty.incubator", name: "netty-incubator-codec-native-quic", version: netty_incubator_quic_version, classifier: osdetector.classifier) + } + + testImplementation project(':rsocket-test') + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'org.assertj:assertj-core' + testImplementation 'org.mockito:mockito-core' + testImplementation 'org.mockito:mockito-junit-jupiter' + testImplementation 'org.junit.jupiter:junit-jupiter-api' + testImplementation 'org.junit.jupiter:junit-jupiter-params' + + testRuntimeOnly 'org.bouncycastle:bcpkix-jdk15on' + testRuntimeOnly 'ch.qos.logback:logback-classic' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' + testRuntimeOnly 'io.netty:netty-tcnative-boringssl-static' + os_suffix +} + +jar { + manifest { + attributes("Automatic-Module-Name": "rsocket.transport.quic") + } +} + +test { + minHeapSize = "512m" + maxHeapSize = "4096m" +} + +description = 'QUIC RSocket transport implementations built on Reactor Netty' diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java b/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java similarity index 92% rename from rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java rename to rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java index 48a262e36..76567fc3b 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java +++ b/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java @@ -59,9 +59,13 @@ public QuicDuplexConnection(String side, Channel connection) { public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof ByteBuf) { ByteBuf byteBuf = (ByteBuf) msg; - ByteBuf frame = FrameLengthCodec.frame(byteBuf).retain(); - byteBuf.release(); - inbound.onNext(frame); + ByteBuf frame = null; + try { + frame = FrameLengthCodec.frame(byteBuf).retain(); + } finally { + byteBuf.release(); + } + inbound.tryEmitNormal(frame); } else { ctx.fireChannelRead(msg); } @@ -69,13 +73,13 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { - inbound.onError(cause); + inbound.tryEmitError(cause); onClose.tryEmitError(cause); } @Override public void channelInactive(ChannelHandlerContext ctx) { - inbound.onError(new ClosedChannelException()); + inbound.tryEmitError(new ClosedChannelException()); onClose.tryEmitEmpty(); ctx.fireChannelInactive(); } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/QuicTransportConfig.java b/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/QuicTransportConfig.java similarity index 100% rename from rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/QuicTransportConfig.java rename to rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/QuicTransportConfig.java diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/QuicClientTransport.java b/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/client/QuicClientTransport.java similarity index 100% rename from rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/QuicClientTransport.java rename to rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/client/QuicClientTransport.java diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/QuicTransportBootstrap.java b/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/internal/QuicTransportBootstrap.java similarity index 100% rename from rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/internal/QuicTransportBootstrap.java rename to rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/internal/QuicTransportBootstrap.java diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/QuicServerTransport.java b/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/server/QuicServerTransport.java similarity index 100% rename from rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/QuicServerTransport.java rename to rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/server/QuicServerTransport.java diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/QuicTransportIntegrationTest.java b/rsocket-transport-quic/src/test/java/io/rsocket/transport/netty/QuicTransportIntegrationTest.java similarity index 100% rename from rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/QuicTransportIntegrationTest.java rename to rsocket-transport-quic/src/test/java/io/rsocket/transport/netty/QuicTransportIntegrationTest.java diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/QuicClientTransportTest.java b/rsocket-transport-quic/src/test/java/io/rsocket/transport/netty/client/QuicClientTransportTest.java similarity index 100% rename from rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/QuicClientTransportTest.java rename to rsocket-transport-quic/src/test/java/io/rsocket/transport/netty/client/QuicClientTransportTest.java diff --git a/scripts/http3-pingpong.sh b/scripts/http3-pingpong.sh index 7bc40b865..e41250df2 100755 --- a/scripts/http3-pingpong.sh +++ b/scripts/http3-pingpong.sh @@ -8,6 +8,7 @@ cd "$ROOT_DIR" GRADLEW="$ROOT_DIR/gradlew" TEST_CLASS_REQUEST_RESPONSE="io.rsocket.transport.netty.Http3Ping.requestResponseTest" TEST_CLASS_REQUEST_STREAM="io.rsocket.transport.netty.Http3Ping.requestStreamTest" +TEST_CLASS_PING_PONG="io.rsocket.transport.netty.Http3PingPongIntegrationTest" PORT="${RSOCKET_HTTP3_PORT:-7878}" SERVER_BIND_HOST="${RSOCKET_HTTP3_BIND_HOST:-0.0.0.0}" @@ -33,7 +34,7 @@ trap cleanup EXIT cat >"$TMP_DIR/http3-javaexec-init.gradle" <<'EOF' allprojects { afterEvaluate { project -> - if (project.path == ':rsocket-transport-netty') { + if (project.path == ':rsocket-transport-h3') { project.tasks.register('runHttp3PongServer', JavaExec) { classpath = project.sourceSets.test.runtimeClasspath mainClass = 'io.rsocket.transport.netty.Http3PongServer' @@ -59,6 +60,13 @@ gradle_base() { "$GRADLEW" --no-daemon --console=plain "$@" } +prepare_http3_build() { + gradle_base \ + :rsocket-transport-netty:jar \ + :rsocket-transport-quic:jar \ + :rsocket-transport-h3:testClasses +} + wait_for_server() { for _ in $(seq 1 60); do if lsof -nP -iUDP:"$PORT" >/dev/null 2>&1; then @@ -71,12 +79,13 @@ wait_for_server() { } start_server_background() { + prepare_http3_build gradle_base \ -DRSOCKET_TEST_HOST="$SERVER_BIND_HOST" \ -DRSOCKET_TEST_PORT="$PORT" \ -DRSOCKET_TEST_PATH="$ROUTE_PATH" \ -I "$TMP_DIR/http3-javaexec-init.gradle" \ - :rsocket-transport-netty:runHttp3PongServer & + :rsocket-transport-h3:runHttp3PongServer & SERVER_PID=$! trap 'stop_server; cleanup' EXIT wait_for_server @@ -93,46 +102,50 @@ stop_server() { run_request_response_test() { TEST_PERF_ENABLED=true \ gradle_base \ - --rerun-tasks \ -I "$TMP_DIR/http3-test-props-init.gradle" \ -DRSOCKET_TEST_HOST="$CLIENT_HOST" \ -DRSOCKET_TEST_PORT="$PORT" \ -DRSOCKET_TEST_PATH="$ROUTE_PATH" \ -DRSOCKET_HTTP3_INTERACTIONS="${1:-$REQUEST_RESPONSE_INTERACTIONS}" \ - :rsocket-transport-netty:test \ + :rsocket-transport-h3:test \ --tests "$TEST_CLASS_REQUEST_RESPONSE" } run_request_stream_test() { TEST_PERF_ENABLED=true \ gradle_base \ - --rerun-tasks \ -I "$TMP_DIR/http3-test-props-init.gradle" \ -DRSOCKET_TEST_HOST="$CLIENT_HOST" \ -DRSOCKET_TEST_PORT="$PORT" \ -DRSOCKET_TEST_PATH="$ROUTE_PATH" \ -DRSOCKET_HTTP3_INTERACTIONS="${1:-$REQUEST_STREAM_INTERACTIONS}" \ - :rsocket-transport-netty:test \ + :rsocket-transport-h3:test \ --tests "$TEST_CLASS_REQUEST_STREAM" } run_integration_test() { gradle_base \ - --rerun-tasks \ - :rsocket-transport-netty:test \ + :rsocket-transport-h3:test \ --tests 'io.rsocket.transport.netty.Http3TransportIntegrationTest' } +run_ping_pong_test() { + gradle_base \ + :rsocket-transport-h3:test \ + --tests "$TEST_CLASS_PING_PONG" +} + usage() { cat < [interactions] Commands: pong-server Run Http3PongServer in the foreground + ping-pong Run the minimal Http3PingPongIntegrationTest request-response Run Http3Ping.requestResponseTest against an existing pong server request-stream Run Http3Ping.requestStreamTest against an existing pong server integration Run Http3TransportIntegrationTest - all Start pong server in the background and run integration, request-response, and request-stream + all Start pong server in the background and run ping-pong, integration, request-response, and request-stream Environment overrides: RSOCKET_HTTP3_PORT UDP port to use (default: $PORT) @@ -147,12 +160,16 @@ EOF COMMAND="${1:-}" case "$COMMAND" in pong-server) + prepare_http3_build exec gradle_base \ -DRSOCKET_TEST_HOST="$SERVER_BIND_HOST" \ -DRSOCKET_TEST_PORT="$PORT" \ -DRSOCKET_TEST_PATH="$ROUTE_PATH" \ -I "$TMP_DIR/http3-javaexec-init.gradle" \ - :rsocket-transport-netty:runHttp3PongServer + :rsocket-transport-h3:runHttp3PongServer + ;; + ping-pong) + run_ping_pong_test ;; request-response) run_request_response_test "${2:-$REQUEST_RESPONSE_INTERACTIONS}" @@ -165,6 +182,7 @@ case "$COMMAND" in ;; all) start_server_background + run_ping_pong_test run_integration_test run_request_response_test "$REQUEST_RESPONSE_INTERACTIONS" run_request_stream_test "$REQUEST_STREAM_INTERACTIONS" diff --git a/settings.gradle b/settings.gradle index 62b838fe2..87ce209f1 100644 --- a/settings.gradle +++ b/settings.gradle @@ -28,6 +28,8 @@ include 'rsocket-micrometer' include 'rsocket-test' include 'rsocket-transport-local' include 'rsocket-transport-netty' +include 'rsocket-transport-quic' +include 'rsocket-transport-h3' include 'rsocket-bom' if (includeAuxiliaryProjects) { From b53e3335f1dea48224deab33d828c95bac1ac499 Mon Sep 17 00:00:00 2001 From: "jeroen.veltman" Date: Thu, 2 Apr 2026 21:19:43 +0200 Subject: [PATCH 06/11] Stabilize QUIC and HTTP/3 transport lifecycle Signed-off-by: jeroen.veltman --- .../core/SetupHandlingDuplexConnection.java | 30 +++- .../resume/ResumableDuplexConnection.java | 5 +- .../netty/Http3DuplexConnection.java | 68 +++++++-- .../transport/netty/QuicDuplexConnection.java | 101 ++++++++++-- .../internal/QuicTransportBootstrap.java | 6 +- .../netty/QuicPingPongIntegrationTest.java | 144 ++++++++++++++++++ .../netty/QuicTransportIntegrationTest.java | 68 +++++++++ 7 files changed, 387 insertions(+), 35 deletions(-) create mode 100644 rsocket-transport-quic/src/test/java/io/rsocket/transport/netty/QuicPingPongIntegrationTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java index 3beedf97f..2e266c46e 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/core/SetupHandlingDuplexConnection.java @@ -6,6 +6,8 @@ import io.rsocket.RSocketErrorException; import java.net.SocketAddress; import java.nio.channels.ClosedChannelException; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; @@ -24,8 +26,10 @@ class SetupHandlingDuplexConnection extends Flux Subscription s; boolean firstFrameReceived = false; + boolean subscriberReady = false; - CoreSubscriber actual; + volatile CoreSubscriber actual; + final Queue pendingFrames = new ConcurrentLinkedQueue<>(); boolean done; Throwable t; @@ -82,6 +86,7 @@ public void subscribe(CoreSubscriber actual) { this.actual = actual; actual.onSubscribe(this); + drainPendingFrames(actual); } @Override @@ -91,7 +96,8 @@ public void request(long n) { return; } - s.request(Long.MAX_VALUE); + subscriberReady = true; + drainPendingFrames(actual); } @Override @@ -104,7 +110,7 @@ public void cancel() { public void onSubscribe(Subscription s) { if (Operators.validate(this.s, s)) { this.s = s; - s.request(1); + s.request(Long.MAX_VALUE); } } @@ -116,7 +122,12 @@ public void onNext(ByteBuf frame) { return; } - actual.onNext(frame); + final CoreSubscriber actual = this.actual; + if (actual != null && subscriberReady) { + actual.onNext(frame); + } else { + pendingFrames.offer(frame); + } } @Override @@ -173,4 +184,15 @@ public ByteBufAllocator alloc() { public String toString() { return "SetupHandlingDuplexConnection{" + "source=" + source + ", done=" + done + '}'; } + + private void drainPendingFrames(CoreSubscriber actual) { + if (actual == null) { + return; + } + + ByteBuf frame; + while ((frame = pendingFrames.poll()) != null) { + actual.onNext(frame); + } + } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java index fe949a62a..5e4c199ae 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -300,7 +300,7 @@ public SocketAddress remoteAddress() { @Override public void request(long n) { if (state == 1 && STATE.compareAndSet(this, 1, 2)) { - // happens for the very first time with the initial connection + // Fallback in case the downstream requests before subscribe() finishes initializing. initConnection(this.activeConnection); } } @@ -315,6 +315,9 @@ public void subscribe(CoreSubscriber receiverSubscriber) { if (state == 0 && STATE.compareAndSet(this, 0, 1)) { receiveSubscriber = receiverSubscriber; receiverSubscriber.onSubscribe(this); + if (STATE.compareAndSet(this, 1, 2)) { + initConnection(this.activeConnection); + } } } diff --git a/rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java b/rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java index eb9861e96..1a128dc66 100644 --- a/rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java +++ b/rsocket-transport-h3/src/main/java/io/rsocket/transport/netty/Http3DuplexConnection.java @@ -41,6 +41,9 @@ public final class Http3DuplexConnection extends BaseDuplexConnection { private final String side; private final Channel connection; private final UnboundedProcessor inbound; + private final Object inboundBufferLock = new Object(); + private boolean handlingInboundData; + private boolean inboundBufferReleasePending; private ByteBuf inboundBuffer; public Http3DuplexConnection(Channel connection) { @@ -143,33 +146,53 @@ public void handleHeaders(Http3HeadersFrame frame) { } public void handleData(Http3DataFrame frame) { + final ByteBuf buffer; try { ByteBuf content = frame.content(); if (!content.isReadable()) { return; } - if (inboundBuffer == null) { - inboundBuffer = alloc().buffer(content.readableBytes()); + synchronized (inboundBufferLock) { + if (inboundBuffer == null) { + inboundBuffer = alloc().buffer(content.readableBytes()); + } + buffer = inboundBuffer; + handlingInboundData = true; } - inboundBuffer.writeBytes(content, content.readerIndex(), content.readableBytes()); - while (inboundBuffer.readableBytes() >= FrameLengthCodec.FRAME_LENGTH_SIZE) { - inboundBuffer.markReaderIndex(); - int frameLength = FrameLengthCodec.length(inboundBuffer); - if (inboundBuffer.readableBytes() < FrameLengthCodec.FRAME_LENGTH_SIZE + frameLength) { - inboundBuffer.resetReaderIndex(); + buffer.writeBytes(content, content.readerIndex(), content.readableBytes()); + + while (buffer.readableBytes() >= FrameLengthCodec.FRAME_LENGTH_SIZE) { + buffer.markReaderIndex(); + int frameLength = FrameLengthCodec.length(buffer); + if (buffer.readableBytes() < FrameLengthCodec.FRAME_LENGTH_SIZE + frameLength) { + buffer.resetReaderIndex(); break; } - inboundBuffer.skipBytes(FrameLengthCodec.FRAME_LENGTH_SIZE); - inbound.tryEmitNormal(inboundBuffer.readRetainedSlice(frameLength)); + buffer.skipBytes(FrameLengthCodec.FRAME_LENGTH_SIZE); + inbound.tryEmitNormal(buffer.readRetainedSlice(frameLength)); + } + + boolean shouldRelease; + synchronized (inboundBufferLock) { + handlingInboundData = false; + if (!inboundBufferReleasePending && buffer.isReadable()) { + buffer.discardReadBytes(); + } + + shouldRelease = inboundBufferReleasePending || !buffer.isReadable(); + if (shouldRelease) { + inboundBufferReleasePending = false; + if (inboundBuffer == buffer) { + inboundBuffer = null; + } + } } - if (inboundBuffer.isReadable()) { - inboundBuffer.discardReadBytes(); - } else { - releaseInboundBuffer(); + if (shouldRelease) { + buffer.release(); } } finally { frame.release(); @@ -188,10 +211,23 @@ public void handleInputClosed() { } private void releaseInboundBuffer() { - if (inboundBuffer != null) { - inboundBuffer.release(); + final ByteBuf bufferToRelease; + synchronized (inboundBufferLock) { + if (inboundBuffer == null) { + return; + } + + if (handlingInboundData) { + inboundBufferReleasePending = true; + return; + } + + bufferToRelease = inboundBuffer; inboundBuffer = null; + inboundBufferReleasePending = false; } + + bufferToRelease.release(); } @Override diff --git a/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java b/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java index 76567fc3b..c6e99ef5b 100644 --- a/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java +++ b/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/QuicDuplexConnection.java @@ -30,7 +30,6 @@ import io.rsocket.frame.FrameLengthCodec; import io.rsocket.internal.BaseDuplexConnection; import io.rsocket.internal.UnboundedProcessor; -import java.nio.channels.ClosedChannelException; import java.net.SocketAddress; import java.util.Objects; import reactor.core.publisher.Flux; @@ -41,6 +40,10 @@ public final class QuicDuplexConnection extends BaseDuplexConnection { private final String side; private final Channel connection; private final UnboundedProcessor inbound; + private final Object inboundBufferLock = new Object(); + private boolean handlingInboundData; + private boolean inboundBufferReleasePending; + private ByteBuf inboundBuffer; public QuicDuplexConnection(Channel connection) { this("unknown", connection); @@ -50,6 +53,17 @@ public QuicDuplexConnection(String side, Channel connection) { this.connection = Objects.requireNonNull(connection, "connection must not be null"); this.side = side; this.inbound = new UnboundedProcessor(); + this.connection + .closeFuture() + .addListener( + future -> { + releaseInboundBuffer(); + if (future.isSuccess()) { + onClose.tryEmitEmpty(); + } else { + onClose.tryEmitError(future.cause()); + } + }); this.connection .pipeline() @@ -58,14 +72,7 @@ public QuicDuplexConnection(String side, Channel connection) { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { if (msg instanceof ByteBuf) { - ByteBuf byteBuf = (ByteBuf) msg; - ByteBuf frame = null; - try { - frame = FrameLengthCodec.frame(byteBuf).retain(); - } finally { - byteBuf.release(); - } - inbound.tryEmitNormal(frame); + handleData((ByteBuf) msg); } else { ctx.fireChannelRead(msg); } @@ -73,13 +80,15 @@ public void channelRead(ChannelHandlerContext ctx, Object msg) { @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + releaseInboundBuffer(); inbound.tryEmitError(cause); onClose.tryEmitError(cause); } @Override public void channelInactive(ChannelHandlerContext ctx) { - inbound.tryEmitError(new ClosedChannelException()); + releaseInboundBuffer(); + inbound.tryEmitComplete(); onClose.tryEmitEmpty(); ctx.fireChannelInactive(); } @@ -103,6 +112,7 @@ public void channelInactive(ChannelHandlerContext ctx) { 1) .doOnError( throwable -> { + releaseInboundBuffer(); onClose.tryEmitError(throwable); this.connection.close(); }) @@ -134,6 +144,7 @@ public SocketAddress remoteAddress() { @Override protected void doOnClose() { + releaseInboundBuffer(); connection.close(); } @@ -162,4 +173,74 @@ public void sendFrame(int streamId, ByteBuf frame) { public String toString() { return "QuicDuplexConnection{" + "side='" + side + '\'' + ", connection=" + connection + '}'; } + + private void handleData(ByteBuf byteBuf) { + final ByteBuf buffer; + try { + if (!byteBuf.isReadable()) { + return; + } + + synchronized (inboundBufferLock) { + if (inboundBuffer == null) { + inboundBuffer = alloc().buffer(byteBuf.readableBytes()); + } + buffer = inboundBuffer; + handlingInboundData = true; + } + + buffer.writeBytes(byteBuf, byteBuf.readerIndex(), byteBuf.readableBytes()); + + while (buffer.readableBytes() >= FrameLengthCodec.FRAME_LENGTH_SIZE) { + buffer.markReaderIndex(); + int frameLength = FrameLengthCodec.length(buffer); + if (buffer.readableBytes() < FrameLengthCodec.FRAME_LENGTH_SIZE + frameLength) { + buffer.resetReaderIndex(); + break; + } + + buffer.skipBytes(FrameLengthCodec.FRAME_LENGTH_SIZE); + inbound.tryEmitNormal(buffer.readRetainedSlice(frameLength)); + } + + boolean shouldRelease; + synchronized (inboundBufferLock) { + handlingInboundData = false; + if (!inboundBufferReleasePending && buffer.isReadable()) { + buffer.discardReadBytes(); + } + + shouldRelease = inboundBufferReleasePending || !buffer.isReadable(); + if (shouldRelease) { + inboundBufferReleasePending = false; + if (inboundBuffer == buffer) { + inboundBuffer = null; + } + } + } + + if (shouldRelease) { + buffer.release(); + } + } finally { + byteBuf.release(); + } + } + + private void releaseInboundBuffer() { + synchronized (inboundBufferLock) { + if (inboundBuffer == null) { + return; + } + + if (handlingInboundData) { + inboundBufferReleasePending = true; + return; + } + + inboundBuffer.release(); + inboundBuffer = null; + inboundBufferReleasePending = false; + } + } } diff --git a/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/internal/QuicTransportBootstrap.java b/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/internal/QuicTransportBootstrap.java index 37ba20219..cc7f14b0e 100644 --- a/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/internal/QuicTransportBootstrap.java +++ b/rsocket-transport-quic/src/main/java/io/rsocket/transport/netty/internal/QuicTransportBootstrap.java @@ -161,10 +161,8 @@ public static Mono bindServer( protected void initChannel(QuicStreamChannel ch) { final DuplexConnection connection = new QuicDuplexConnection("server", ch); - acceptor - .apply(connection) - .doFinally(__ -> ch.close()) - .subscribe(null, t -> ch.close()); + acceptor.apply(connection).subscribe(null, t -> ch.close()); + connection.onClose().doFinally(__ -> ch.close()).subscribe(); } }) .build(); diff --git a/rsocket-transport-quic/src/test/java/io/rsocket/transport/netty/QuicPingPongIntegrationTest.java b/rsocket-transport-quic/src/test/java/io/rsocket/transport/netty/QuicPingPongIntegrationTest.java new file mode 100644 index 000000000..69e3e8a07 --- /dev/null +++ b/rsocket-transport-quic/src/test/java/io/rsocket/transport/netty/QuicPingPongIntegrationTest.java @@ -0,0 +1,144 @@ +/* + * Copyright 2015-2026 the original author or 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.rsocket.transport.netty; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.transport.netty.client.QuicClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.QuicServerTransport; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +class QuicPingPongIntegrationTest { + + @Test + void shouldReturnPongForPing() throws Exception { + AtomicBoolean serverReceivedRequest = new AtomicBoolean(); + try (TestContext context = + TestContext.create( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + serverReceivedRequest.set(true); + try { + assertThat(payload.getDataUtf8()).isEqualTo("PING"); + } finally { + payload.release(); + } + return Mono.just(DefaultPayload.create("PONG")); + } + })) { + Throwable failure = null; + String response = null; + try { + response = + context.client + .requestResponse(DefaultPayload.create("PING")) + .map(QuicPingPongIntegrationTest::releaseToString) + .block(Duration.ofSeconds(10)); + } catch (Throwable t) { + failure = t; + } + + assertThat(serverReceivedRequest) + .withFailMessage("Server never observed the request before the client timed out") + .matches(AtomicBoolean::get); + assertThat(failure).isNull(); + assertThat(response).isEqualTo("PONG"); + } + } + + private static String releaseToString(Payload payload) { + try { + return payload.getDataUtf8(); + } finally { + payload.release(); + } + } + + private static final class TestContext implements AutoCloseable { + + private final SelfSignedCertificate certificate; + private final CloseableChannel server; + private final io.rsocket.RSocket client; + + private TestContext( + SelfSignedCertificate certificate, CloseableChannel server, io.rsocket.RSocket client) { + this.certificate = certificate; + this.server = server; + this.client = client; + } + + static TestContext create(RSocket responder) throws Exception { + SelfSignedCertificate certificate = new SelfSignedCertificate(); + QuicSslContext serverSslContext = + QuicSslContextBuilder.forServer( + certificate.privateKey(), null, certificate.certificate()) + .applicationProtocols("rsocket") + .build(); + QuicSslContext clientSslContext = + QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols("rsocket") + .build(); + + CloseableChannel server = + RSocketServer.create((setup, sendingSocket) -> Mono.just(responder)) + .bind( + QuicServerTransport.create(0) + .config(QuicTransportConfig.builder().sslContext(serverSslContext).build())) + .block(Duration.ofSeconds(10)); + + io.rsocket.RSocket client = + RSocketConnector.connectWith( + QuicClientTransport.create("127.0.0.1", server.address().getPort()) + .config( + QuicTransportConfig.builder() + .sslContext(clientSslContext) + .validateCertificates(false) + .build())) + .block(Duration.ofSeconds(10)); + + return new TestContext(certificate, server, client); + } + + @Override + public void close() throws Exception { + try { + client.dispose(); + } finally { + try { + server.dispose(); + } finally { + certificate.delete(); + } + } + } + } +} diff --git a/rsocket-transport-quic/src/test/java/io/rsocket/transport/netty/QuicTransportIntegrationTest.java b/rsocket-transport-quic/src/test/java/io/rsocket/transport/netty/QuicTransportIntegrationTest.java index 340a82e21..c46838793 100644 --- a/rsocket-transport-quic/src/test/java/io/rsocket/transport/netty/QuicTransportIntegrationTest.java +++ b/rsocket-transport-quic/src/test/java/io/rsocket/transport/netty/QuicTransportIntegrationTest.java @@ -99,4 +99,72 @@ void shouldConnectAndExchangeFrames() throws Exception { certificate.delete(); } } + + @Test + void shouldReceiveMultipleFramesFromClient() throws Exception { + SelfSignedCertificate certificate = new SelfSignedCertificate(); + QuicSslContext serverSslContext = + QuicSslContextBuilder.forServer( + certificate.privateKey(), null, certificate.certificate()) + .applicationProtocols("rsocket") + .build(); + QuicSslContext clientSslContext = + QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols("rsocket") + .build(); + + Sinks.One accepted = Sinks.one(); + CloseableChannel server = + QuicServerTransport.create(0) + .config(QuicTransportConfig.builder().sslContext(serverSslContext).build()) + .start( + connection -> { + accepted.tryEmitValue(connection); + return connection.onClose(); + }) + .block(Duration.ofSeconds(10)); + + DuplexConnection client = + QuicClientTransport.create("127.0.0.1", server.address().getPort()) + .config( + QuicTransportConfig.builder() + .sslContext(clientSslContext) + .validateCertificates(false) + .build()) + .connect() + .block(Duration.ofSeconds(10)); + + client.sendFrame(1, Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4})); + client.sendFrame(3, Unpooled.wrappedBuffer(new byte[] {5, 6, 7, 8})); + + DuplexConnection serverConnection = accepted.asMono().block(Duration.ofSeconds(10)); + + java.util.List received = + serverConnection.receive().take(2).collectList().block(Duration.ofSeconds(10)); + assertThat(received).hasSize(2); + ByteBuf first = received.get(0); + ByteBuf second = received.get(1); + + try { + assertThat(first.readableBytes()).isEqualTo(4); + assertThat(first.readByte()).isEqualTo((byte) 1); + assertThat(first.readByte()).isEqualTo((byte) 2); + assertThat(first.readByte()).isEqualTo((byte) 3); + assertThat(first.readByte()).isEqualTo((byte) 4); + + assertThat(second.readableBytes()).isEqualTo(4); + assertThat(second.readByte()).isEqualTo((byte) 5); + assertThat(second.readByte()).isEqualTo((byte) 6); + assertThat(second.readByte()).isEqualTo((byte) 7); + assertThat(second.readByte()).isEqualTo((byte) 8); + } finally { + first.release(); + second.release(); + client.dispose(); + serverConnection.dispose(); + server.dispose(); + certificate.delete(); + } + } } From 0e5cf30c373c767d50fb4d9a1254cd0b35973afd Mon Sep 17 00:00:00 2001 From: "jeroen.veltman" Date: Thu, 2 Apr 2026 21:19:43 +0200 Subject: [PATCH 07/11] Add QUIC and HTTP/3 transport examples Signed-off-by: jeroen.veltman --- rsocket-examples/build.gradle | 7 + .../transport/h3/Http3TransportFactory.java | 109 ++++++++ .../h3/channel/ChannelEchoClient.java | 60 +++++ .../h3/client/RSocketClientExample.java | 54 ++++ ...ingWithServerSideNotificationsExample.java | 236 ++++++++++++++++++ .../lease/advanced/common/LeaseManager.java | 146 +++++++++++ .../common/LimitBasedLeaseSender.java | 56 +++++ .../common/LimitBasedStatsCollector.java | 75 ++++++ .../h3/lease/advanced/controller/Task.java | 29 +++ .../controller/TasksHandlingRSocket.java | 46 ++++ .../advanced/invertmulticlient/README.MD | 0 .../invertmulticlient/RequestingServer.java | 78 ++++++ .../invertmulticlient/RespondingClient.java | 67 +++++ .../h3/lease/advanced/multiclient/README.MD | 0 .../multiclient/RequestingClient.java | 41 +++ .../multiclient/RespondingServer.java | 81 ++++++ .../h3/lease/simple/LeaseExample.java | 159 ++++++++++++ .../RoundRobinRSocketLoadbalancerExample.java | 109 ++++++++ .../routing/CompositeMetadataExample.java | 101 ++++++++ .../routing/RoutingMetadataExample.java | 82 ++++++ .../plugins/LimitRateInterceptorExample.java | 82 ++++++ .../h3/requestresponse/HelloWorldClient.java | 68 +++++ .../h3/resume/ResumeFileTransfer.java | 113 +++++++++ .../examples/transport/h3/resume/readme.md | 22 ++ .../h3/stream/ClientStreamingToServer.java | 62 +++++ .../h3/stream/ServerStreamingToClient.java | 59 +++++ .../transport/quic/QuicTransportFactory.java | 99 ++++++++ .../quic/channel/ChannelEchoClient.java | 60 +++++ .../quic/client/RSocketClientExample.java | 54 ++++ ...ingWithServerSideNotificationsExample.java | 236 ++++++++++++++++++ .../lease/advanced/common/LeaseManager.java | 146 +++++++++++ .../common/LimitBasedLeaseSender.java | 56 +++++ .../common/LimitBasedStatsCollector.java | 75 ++++++ .../quic/lease/advanced/controller/Task.java | 29 +++ .../controller/TasksHandlingRSocket.java | 46 ++++ .../advanced/invertmulticlient/README.MD | 0 .../invertmulticlient/RequestingServer.java | 78 ++++++ .../invertmulticlient/RespondingClient.java | 67 +++++ .../quic/lease/advanced/multiclient/README.MD | 0 .../multiclient/RequestingClient.java | 41 +++ .../multiclient/RespondingServer.java | 81 ++++++ .../quic/lease/simple/LeaseExample.java | 159 ++++++++++++ .../RoundRobinRSocketLoadbalancerExample.java | 109 ++++++++ .../routing/CompositeMetadataExample.java | 101 ++++++++ .../routing/RoutingMetadataExample.java | 82 ++++++ .../plugins/LimitRateInterceptorExample.java | 82 ++++++ .../requestresponse/HelloWorldClient.java | 73 ++++++ .../quic/resume/ResumeFileTransfer.java | 119 +++++++++ .../examples/transport/quic/resume/readme.md | 22 ++ .../quic/stream/ClientStreamingToServer.java | 62 +++++ .../quic/stream/ServerStreamingToClient.java | 59 +++++ .../DisconnectableClientTransport.java | 83 ++++++ .../support/ResumeExampleSupport.java | 208 +++++++++++++++ .../Files.java => support/ResumeFiles.java} | 64 ++++- .../transport/support/ResumeRequest.java | 35 +++ .../transport/support/ResumeRequestCodec.java | 36 +++ .../tcp/resume/ResumeFileTransfer.java | 71 ++---- scripts/run-example.sh | 213 ++++++++++++++++ 58 files changed, 4530 insertions(+), 58 deletions(-) create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/Http3TransportFactory.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/channel/ChannelEchoClient.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/client/RSocketClientExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/fnf/TaskProcessingWithServerSideNotificationsExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/common/LeaseManager.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/common/LimitBasedLeaseSender.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/common/LimitBasedStatsCollector.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/controller/Task.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/controller/TasksHandlingRSocket.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/invertmulticlient/README.MD create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/invertmulticlient/RequestingServer.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/invertmulticlient/RespondingClient.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/multiclient/README.MD create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/multiclient/RequestingClient.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/multiclient/RespondingServer.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/simple/LeaseExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/loadbalancer/RoundRobinRSocketLoadbalancerExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/CompositeMetadataExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/RoutingMetadataExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/plugins/LimitRateInterceptorExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/requestresponse/HelloWorldClient.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/resume/ResumeFileTransfer.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/resume/readme.md create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ClientStreamingToServer.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ServerStreamingToClient.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/QuicTransportFactory.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/channel/ChannelEchoClient.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/client/RSocketClientExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/fnf/TaskProcessingWithServerSideNotificationsExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/common/LeaseManager.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/common/LimitBasedLeaseSender.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/common/LimitBasedStatsCollector.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/controller/Task.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/controller/TasksHandlingRSocket.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/invertmulticlient/README.MD create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/invertmulticlient/RequestingServer.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/invertmulticlient/RespondingClient.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/multiclient/README.MD create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/multiclient/RequestingClient.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/multiclient/RespondingServer.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/simple/LeaseExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/loadbalancer/RoundRobinRSocketLoadbalancerExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/CompositeMetadataExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/RoutingMetadataExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/plugins/LimitRateInterceptorExample.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/requestresponse/HelloWorldClient.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/resume/ResumeFileTransfer.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/resume/readme.md create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ClientStreamingToServer.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ServerStreamingToClient.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/support/DisconnectableClientTransport.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeExampleSupport.java rename rsocket-examples/src/main/java/io/rsocket/examples/transport/{tcp/resume/Files.java => support/ResumeFiles.java} (62%) create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeRequest.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeRequestCodec.java create mode 100755 scripts/run-example.sh diff --git a/rsocket-examples/build.gradle b/rsocket-examples/build.gradle index 4059eb957..c8b255d63 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -18,11 +18,18 @@ plugins { id 'java' } +def netty_incubator_quic_version = "0.0.73.Final" +def netty_incubator_http3_version = "0.0.30.Final" + dependencies { implementation project(':rsocket-core') implementation project(':rsocket-load-balancer') + implementation project(':rsocket-transport-h3') implementation project(':rsocket-transport-local') implementation project(':rsocket-transport-netty') + implementation project(':rsocket-transport-quic') + implementation "io.netty.incubator:netty-incubator-codec-classes-quic:${netty_incubator_quic_version}" + implementation "io.netty.incubator:netty-incubator-codec-http3:${netty_incubator_http3_version}" implementation "io.micrometer:micrometer-core" implementation "io.micrometer:micrometer-tracing" diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/Http3TransportFactory.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/Http3TransportFactory.java new file mode 100644 index 000000000..cb2ea43a8 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/Http3TransportFactory.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015-2026 the original author or 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.rsocket.examples.transport.h3; + +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.incubator.codec.http3.Http3; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; +import io.rsocket.transport.netty.Http3TransportConfig; +import io.rsocket.transport.netty.QuicTransportConfig; +import io.rsocket.transport.netty.client.Http3ClientTransport; +import io.rsocket.transport.netty.server.Http3ServerTransport; +import java.net.InetSocketAddress; + +public final class Http3TransportFactory { + + private static final String DEFAULT_HOST = "localhost"; + private static final String DEFAULT_PATH = "/rsocket"; + private static final SelfSignedCertificate CERTIFICATE = createCertificate(); + private static final Http3TransportConfig SERVER_CONFIG = createServerConfig(); + private static final Http3TransportConfig CLIENT_CONFIG = createClientConfig(); + + static { + Runtime.getRuntime().addShutdownHook(new Thread(CERTIFICATE::delete)); + } + + private Http3TransportFactory() {} + + public static Http3ClientTransport client(int port) { + return client(DEFAULT_HOST, port); + } + + public static Http3ClientTransport client(String host, int port) { + return Http3ClientTransport.create(host, port).config(CLIENT_CONFIG); + } + + public static Http3ClientTransport client(InetSocketAddress address) { + return client(address.getHostString(), address.getPort()); + } + + public static Http3ServerTransport server(int port) { + return server(DEFAULT_HOST, port); + } + + public static Http3ServerTransport server(String host, int port) { + return Http3ServerTransport.create(host, port).config(SERVER_CONFIG); + } + + private static SelfSignedCertificate createCertificate() { + try { + return new SelfSignedCertificate(); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + private static Http3TransportConfig createServerConfig() { + try { + QuicSslContext serverSslContext = + QuicSslContextBuilder.forServer( + CERTIFICATE.privateKey(), null, CERTIFICATE.certificate()) + .applicationProtocols(Http3.supportedApplicationProtocols()) + .build(); + + return Http3TransportConfig.builder() + .path(DEFAULT_PATH) + .quicConfig(QuicTransportConfig.builder().sslContext(serverSslContext).build()) + .build(); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + private static Http3TransportConfig createClientConfig() { + try { + QuicSslContext clientSslContext = + QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols(Http3.supportedApplicationProtocols()) + .build(); + + return Http3TransportConfig.builder() + .path(DEFAULT_PATH) + .quicConfig( + QuicTransportConfig.builder() + .sslContext(clientSslContext) + .validateCertificates(false) + .build()) + .build(); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/channel/ChannelEchoClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/channel/ChannelEchoClient.java new file mode 100644 index 000000000..f5f4426fd --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/channel/ChannelEchoClient.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.h3.channel; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +public final class ChannelEchoClient { + + private static final Logger logger = LoggerFactory.getLogger(ChannelEchoClient.class); + + public static void main(String[] args) { + + SocketAcceptor echoAcceptor = + SocketAcceptor.forRequestChannel( + payloads -> + Flux.from(payloads) + .map(Payload::getDataUtf8) + .map(s -> "Echo: " + s) + .map(DefaultPayload::create)); + + RSocketServer.create(echoAcceptor).bindNow(Http3TransportFactory.server("localhost", 7000)); + + RSocket socket = + RSocketConnector.connectWith(Http3TransportFactory.client("localhost", 7000)).block(); + + socket + .requestChannel( + Flux.interval(Duration.ofMillis(1000)).map(i -> DefaultPayload.create("Hello"))) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .doFinally(signalType -> socket.dispose()) + .then() + .block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/client/RSocketClientExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/client/RSocketClientExample.java new file mode 100644 index 000000000..4a702379b --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/client/RSocketClientExample.java @@ -0,0 +1,54 @@ +package io.rsocket.examples.transport.h3.client; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketClient; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +public class RSocketClientExample { + static final Logger logger = LoggerFactory.getLogger(RSocketClientExample.class); + + public static void main(String[] args) { + + RSocketServer.create( + SocketAcceptor.forRequestResponse( + p -> { + String data = p.getDataUtf8(); + logger.info("Received request data {}", data); + + Payload responsePayload = DefaultPayload.create("Echo: " + data); + p.release(); + + return Mono.just(responsePayload); + })) + .bind(Http3TransportFactory.server("localhost", 7000)) + .delaySubscription(Duration.ofSeconds(5)) + .doOnNext(cc -> logger.info("Server started on the address : {}", cc.address())) + .block(); + + Mono source = + RSocketConnector.create() + .reconnect(Retry.backoff(50, Duration.ofMillis(500))) + .connect(Http3TransportFactory.client("localhost", 7000)); + + RSocketClient.from(source) + .requestResponse(Mono.just(DefaultPayload.create("Test Request"))) + .doOnSubscribe(s -> logger.info("Executing Request")) + .doOnNext( + d -> { + logger.info("Received response data {}", d.getDataUtf8()); + d.release(); + }) + .repeat(10) + .blockLast(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/fnf/TaskProcessingWithServerSideNotificationsExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/fnf/TaskProcessingWithServerSideNotificationsExample.java new file mode 100644 index 000000000..0f75a7cb8 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/fnf/TaskProcessingWithServerSideNotificationsExample.java @@ -0,0 +1,236 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.h3.fnf; + +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadLocalRandom; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; +import reactor.util.concurrent.Queues; + +/** + * An example of long-running tasks processing (a.k.a Kafka style) where a client submits tasks over + * request `FireAndForget` and then receives results over the same method but on it is own side. + * + *

This example shows a case when the client may disappear, however, another a client can connect + * again and receive undelivered completed tasks remaining for the previous one. + */ +public class TaskProcessingWithServerSideNotificationsExample { + + public static void main(String[] args) throws InterruptedException { + Sinks.Many tasksProcessor = + Sinks.many().unicast().onBackpressureBuffer(Queues.unboundedMultiproducer().get()); + ConcurrentMap> idToCompletedTasksMap = new ConcurrentHashMap<>(); + ConcurrentMap idToRSocketMap = new ConcurrentHashMap<>(); + BackgroundWorker backgroundWorker = + new BackgroundWorker(tasksProcessor.asFlux(), idToCompletedTasksMap, idToRSocketMap); + + RSocketServer.create(new TasksAcceptor(tasksProcessor, idToCompletedTasksMap, idToRSocketMap)) + .bindNow(Http3TransportFactory.server(9991)); + + Logger logger = LoggerFactory.getLogger("RSocket.Client.ID[Test]"); + + Mono rSocketMono = + RSocketConnector.create() + .setupPayload(DefaultPayload.create("Test")) + .acceptor( + SocketAcceptor.forFireAndForget( + p -> { + logger.info("Received Processed Task[{}]", p.getDataUtf8()); + p.release(); + return Mono.empty(); + })) + .connect(Http3TransportFactory.client(9991)); + + RSocket rSocketRequester1 = rSocketMono.block(); + + for (int i = 0; i < 10; i++) { + rSocketRequester1.fireAndForget(DefaultPayload.create("task" + i)).block(); + } + + Thread.sleep(4000); + + rSocketRequester1.dispose(); + logger.info("Disposed"); + + Thread.sleep(4000); + + RSocket rSocketRequester2 = rSocketMono.block(); + + logger.info("Reconnected"); + + Thread.sleep(10000); + } + + static class BackgroundWorker extends BaseSubscriber { + final ConcurrentMap> idToCompletedTasksMap; + final ConcurrentMap idToRSocketMap; + + BackgroundWorker( + Flux taskProducer, + ConcurrentMap> idToCompletedTasksMap, + ConcurrentMap idToRSocketMap) { + + this.idToCompletedTasksMap = idToCompletedTasksMap; + this.idToRSocketMap = idToRSocketMap; + + // mimic a long running task processing + taskProducer + .concatMap( + t -> + Mono.delay(Duration.ofMillis(ThreadLocalRandom.current().nextInt(200, 2000))) + .thenReturn(t)) + .subscribe(this); + } + + @Override + protected void hookOnNext(Task task) { + BlockingQueue completedTasksQueue = + idToCompletedTasksMap.computeIfAbsent(task.id, __ -> new LinkedBlockingQueue<>()); + + completedTasksQueue.offer(task); + RSocket rSocket = idToRSocketMap.get(task.id); + if (rSocket != null) { + rSocket + .fireAndForget(DefaultPayload.create(task.content)) + .subscribe(null, e -> {}, () -> completedTasksQueue.remove(task)); + } + } + } + + static class TasksAcceptor implements SocketAcceptor { + + static final Logger logger = LoggerFactory.getLogger(TasksAcceptor.class); + + final Sinks.Many tasksToProcess; + final ConcurrentMap> idToCompletedTasksMap; + final ConcurrentMap idToRSocketMap; + + TasksAcceptor( + Sinks.Many tasksToProcess, + ConcurrentMap> idToCompletedTasksMap, + ConcurrentMap idToRSocketMap) { + this.tasksToProcess = tasksToProcess; + this.idToCompletedTasksMap = idToCompletedTasksMap; + this.idToRSocketMap = idToRSocketMap; + } + + @Override + public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) { + String id = setup.getDataUtf8(); + logger.info("Accepting a new client connection with ID {}", id); + // sendingRSocket represents here an RSocket requester to a remote peer + + if (this.idToRSocketMap.compute( + id, (__, old) -> old == null || old.isDisposed() ? sendingSocket : old) + == sendingSocket) { + return Mono.just( + new RSocketTaskHandler(idToRSocketMap, tasksToProcess, id, sendingSocket)) + .doOnSuccess(__ -> checkTasksToDeliver(sendingSocket, id)); + } + + return Mono.error( + new IllegalStateException("There is already a client connected with the same ID")); + } + + private void checkTasksToDeliver(RSocket sendingSocket, String id) { + logger.info("Accepted a new client connection with ID {}. Checking for remaining tasks", id); + BlockingQueue tasksToDeliver = this.idToCompletedTasksMap.get(id); + + if (tasksToDeliver == null || tasksToDeliver.isEmpty()) { + // means nothing yet to send + return; + } + + logger.info("Found remaining tasks to deliver for client {}", id); + + for (; ; ) { + Task task = tasksToDeliver.poll(); + + if (task == null) { + return; + } + + sendingSocket + .fireAndForget(DefaultPayload.create(task.content)) + .subscribe( + null, + e -> { + // offers back a task if it has not been delivered + tasksToDeliver.offer(task); + }); + } + } + + private static class RSocketTaskHandler implements RSocket { + + private final String id; + private final RSocket sendingSocket; + private ConcurrentMap idToRSocketMap; + private Sinks.Many tasksToProcess; + + public RSocketTaskHandler( + ConcurrentMap idToRSocketMap, + Sinks.Many tasksToProcess, + String id, + RSocket sendingSocket) { + this.id = id; + this.sendingSocket = sendingSocket; + this.idToRSocketMap = idToRSocketMap; + this.tasksToProcess = tasksToProcess; + } + + @Override + public Mono fireAndForget(Payload payload) { + logger.info("Received a Task[{}] from Client.ID[{}]", payload.getDataUtf8(), id); + Sinks.EmitResult result = tasksToProcess.tryEmitNext(new Task(id, payload.getDataUtf8())); + payload.release(); + return result.isFailure() ? Mono.error(new Sinks.EmissionException(result)) : Mono.empty(); + } + + @Override + public void dispose() { + idToRSocketMap.remove(id, sendingSocket); + } + } + } + + static class Task { + final String id; + final String content; + + Task(String id, String content) { + this.id = id; + this.content = content; + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/common/LeaseManager.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/common/LeaseManager.java new file mode 100644 index 000000000..20cba9908 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/common/LeaseManager.java @@ -0,0 +1,146 @@ +package io.rsocket.examples.transport.h3.lease.advanced.common; + +import io.rsocket.examples.transport.h3.Http3TransportFactory; + +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +public class LeaseManager implements Runnable { + + static final Logger logger = LoggerFactory.getLogger(LeaseManager.class); + + volatile int activeConnectionsCount; + static final AtomicIntegerFieldUpdater ACTIVE_CONNECTIONS_COUNT = + AtomicIntegerFieldUpdater.newUpdater(LeaseManager.class, "activeConnectionsCount"); + + volatile int stateAndInFlight; + static final AtomicIntegerFieldUpdater STATE_AND_IN_FLIGHT = + AtomicIntegerFieldUpdater.newUpdater(LeaseManager.class, "stateAndInFlight"); + + static final int MASK_PAUSED = 0b1_000_0000_0000_0000_0000_0000_0000_0000; + static final int MASK_IN_FLIGHT = 0b0_111_1111_1111_1111_1111_1111_1111_1111; + + final BlockingDeque sendersQueue = new LinkedBlockingDeque<>(); + final Scheduler worker = Schedulers.newSingle(LeaseManager.class.getName()); + + final int capacity; + final int ttl; + + public LeaseManager(int capacity, int ttl) { + this.capacity = capacity; + this.ttl = ttl; + } + + @Override + public void run() { + try { + LimitBasedLeaseSender leaseSender = sendersQueue.poll(); + + if (leaseSender == null) { + return; + } + + if (leaseSender.isDisposed()) { + logger.debug("Connection[" + leaseSender.connectionId + "]: LeaseSender is Disposed"); + worker.schedule(this); + return; + } + + int limit = leaseSender.limitAlgorithm.getLimit(); + + if (limit == 0) { + throw new IllegalStateException("Limit is 0"); + } + + if (pauseIfNoCapacity()) { + sendersQueue.addFirst(leaseSender); + logger.debug("Pause execution. Not enough capacity"); + return; + } + + leaseSender.sendLease(ttl, limit); + sendersQueue.offer(leaseSender); + + int activeConnections = activeConnectionsCount; + int nextDelay = activeConnections == 0 ? ttl : (ttl / activeConnections); + + logger.debug("Next check happens in " + nextDelay + "ms"); + + worker.schedule(this, nextDelay, TimeUnit.MILLISECONDS); + } catch (Throwable e) { + logger.error("LeaseSender failed to send lease", e); + } + } + + int incrementInFlightAndGet() { + for (; ; ) { + int state = stateAndInFlight; + int paused = state & MASK_PAUSED; + int inFlight = stateAndInFlight & MASK_IN_FLIGHT; + + // assume overflow is impossible due to max concurrency in RSocket it self + int nextInFlight = inFlight + 1; + + if (STATE_AND_IN_FLIGHT.compareAndSet(this, state, nextInFlight | paused)) { + return nextInFlight; + } + } + } + + void decrementInFlight() { + for (; ; ) { + int state = stateAndInFlight; + int paused = state & MASK_PAUSED; + int inFlight = stateAndInFlight & MASK_IN_FLIGHT; + + // assume overflow is impossible due to max concurrency in RSocket it self + int nextInFlight = inFlight - 1; + + if (inFlight == capacity && paused == MASK_PAUSED) { + if (STATE_AND_IN_FLIGHT.compareAndSet(this, state, nextInFlight)) { + logger.debug("Resume execution"); + worker.schedule(this); + return; + } + } else { + if (STATE_AND_IN_FLIGHT.compareAndSet(this, state, nextInFlight | paused)) { + return; + } + } + } + } + + boolean pauseIfNoCapacity() { + int capacity = this.capacity; + for (; ; ) { + int inFlight = stateAndInFlight; + + if (inFlight < capacity) { + return false; + } + + if (STATE_AND_IN_FLIGHT.compareAndSet(this, inFlight, inFlight | MASK_PAUSED)) { + return true; + } + } + } + + void unregister() { + ACTIVE_CONNECTIONS_COUNT.decrementAndGet(this); + } + + void register(LimitBasedLeaseSender sender) { + sendersQueue.offer(sender); + final int activeCount = ACTIVE_CONNECTIONS_COUNT.getAndIncrement(this); + + if (activeCount == 0) { + worker.schedule(this); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/common/LimitBasedLeaseSender.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/common/LimitBasedLeaseSender.java new file mode 100644 index 000000000..af2b8d2b9 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/common/LimitBasedLeaseSender.java @@ -0,0 +1,56 @@ +package io.rsocket.examples.transport.h3.lease.advanced.common; + +import io.rsocket.examples.transport.h3.Http3TransportFactory; + +import com.netflix.concurrency.limits.Limit; +import io.rsocket.lease.Lease; +import io.rsocket.lease.TrackingLeaseSender; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Sinks; +import reactor.util.concurrent.Queues; + +public class LimitBasedLeaseSender extends LimitBasedStatsCollector implements TrackingLeaseSender { + + static final Logger logger = LoggerFactory.getLogger(LimitBasedLeaseSender.class); + + final String connectionId; + final Sinks.Many sink = + Sinks.many().unicast().onBackpressureBuffer(Queues.one().get()); + + public LimitBasedLeaseSender( + String connectionId, LeaseManager leaseManager, Limit limitAlgorithm) { + super(leaseManager, limitAlgorithm); + this.connectionId = connectionId; + } + + @Override + public Flux send() { + logger.info("Received new leased Connection[" + connectionId + "]"); + + leaseManager.register(this); + + return sink.asFlux(); + } + + public void sendLease(int ttl, int amount) { + final Lease nextLease = Lease.create(Duration.ofMillis(ttl), amount); + final Sinks.EmitResult result = sink.tryEmitNext(nextLease); + + if (result.isFailure()) { + logger.warn( + "Connection[" + + connectionId + + "]. Issued Lease: [" + + nextLease + + "] was not sent due to " + + result); + } else { + if (logger.isDebugEnabled()) { + logger.debug("To Connection[" + connectionId + "]: Issued Lease: [" + nextLease + "]"); + } + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/common/LimitBasedStatsCollector.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/common/LimitBasedStatsCollector.java new file mode 100644 index 000000000..8bd4767da --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/common/LimitBasedStatsCollector.java @@ -0,0 +1,75 @@ +package io.rsocket.examples.transport.h3.lease.advanced.common; + +import io.rsocket.examples.transport.h3.Http3TransportFactory; + +import com.netflix.concurrency.limits.Limit; +import io.netty.buffer.ByteBuf; +import io.rsocket.frame.FrameType; +import io.rsocket.plugins.RequestInterceptor; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.LongSupplier; +import reactor.util.annotation.Nullable; + +public class LimitBasedStatsCollector extends AtomicBoolean implements RequestInterceptor { + + final LeaseManager leaseManager; + final Limit limitAlgorithm; + + final ConcurrentMap inFlightMap = new ConcurrentHashMap<>(); + final ConcurrentMap timeMap = new ConcurrentHashMap<>(); + + final LongSupplier clock = System::nanoTime; + + public LimitBasedStatsCollector(LeaseManager leaseManager, Limit limitAlgorithm) { + this.leaseManager = leaseManager; + this.limitAlgorithm = limitAlgorithm; + } + + @Override + public void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metadata) { + long startTime = clock.getAsLong(); + + int currentInFlight = leaseManager.incrementInFlightAndGet(); + + inFlightMap.put(streamId, currentInFlight); + timeMap.put(streamId, startTime); + } + + @Override + public void onReject( + Throwable rejectionReason, FrameType requestType, @Nullable ByteBuf metadata) {} + + @Override + public void onTerminate(int streamId, FrameType requestType, @Nullable Throwable t) { + leaseManager.decrementInFlight(); + + Long startTime = timeMap.remove(streamId); + Integer currentInflight = inFlightMap.remove(streamId); + + limitAlgorithm.onSample(startTime, clock.getAsLong() - startTime, currentInflight, t != null); + } + + @Override + public void onCancel(int streamId, FrameType requestType) { + leaseManager.decrementInFlight(); + + Long startTime = timeMap.remove(streamId); + Integer currentInflight = inFlightMap.remove(streamId); + + limitAlgorithm.onSample(startTime, clock.getAsLong() - startTime, currentInflight, true); + } + + @Override + public boolean isDisposed() { + return get(); + } + + @Override + public void dispose() { + if (!getAndSet(true)) { + leaseManager.unregister(); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/controller/Task.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/controller/Task.java new file mode 100644 index 000000000..72b56a788 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/controller/Task.java @@ -0,0 +1,29 @@ +package io.rsocket.examples.transport.h3.lease.advanced.controller; + +import io.rsocket.examples.transport.h3.Http3TransportFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// emulating a worker that process data from the queue +public class Task implements Runnable { + private static final Logger logger = LoggerFactory.getLogger(Task.class); + + final String message; + final int processingTime; + + Task(String message, int processingTime) { + this.message = message; + this.processingTime = processingTime; + } + + @Override + public void run() { + logger.info("Processing Task[{}]", message); + try { + Thread.sleep(processingTime); // emulating processing + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/controller/TasksHandlingRSocket.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/controller/TasksHandlingRSocket.java new file mode 100644 index 000000000..9e1544afc --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/controller/TasksHandlingRSocket.java @@ -0,0 +1,46 @@ +package io.rsocket.examples.transport.h3.lease.advanced.controller; + +import io.rsocket.examples.transport.h3.Http3TransportFactory; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +public class TasksHandlingRSocket implements RSocket { + + private static final Logger logger = LoggerFactory.getLogger(TasksHandlingRSocket.class); + + final Disposable terminatable; + final Scheduler workScheduler; + final int processingTime; + + public TasksHandlingRSocket(Disposable terminatable, Scheduler scheduler, int processingTime) { + this.terminatable = terminatable; + this.workScheduler = scheduler; + this.processingTime = processingTime; + } + + @Override + public Mono fireAndForget(Payload payload) { + + // specifically to show that lease can limit rate of fnf requests in + // that example + String message = payload.getDataUtf8(); + payload.release(); + + return Mono.fromRunnable(new Task(message, processingTime)) + // schedule task on specific, limited in size scheduler + .subscribeOn(workScheduler) + // if errors - terminates server + .doOnError( + t -> { + logger.error("Queue has been overflowed. Terminating server"); + terminatable.dispose(); + System.exit(9); + }); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/invertmulticlient/README.MD b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/invertmulticlient/README.MD new file mode 100644 index 000000000..e69de29bb diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/invertmulticlient/RequestingServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/invertmulticlient/RequestingServer.java new file mode 100644 index 000000000..78eac1ec5 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/invertmulticlient/RequestingServer.java @@ -0,0 +1,78 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.h3.lease.advanced.invertmulticlient; + +import io.rsocket.RSocket; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.util.ByteBufPayload; +import java.util.Comparator; +import java.util.concurrent.PriorityBlockingQueue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class RequestingServer { + + private static final Logger logger = LoggerFactory.getLogger(RequestingServer.class); + + public static void main(String[] args) { + PriorityBlockingQueue rSockets = + new PriorityBlockingQueue<>( + 16, Comparator.comparingDouble(RSocket::availability).reversed()); + + CloseableChannel server = + RSocketServer.create( + (setup, sendingSocket) -> { + logger.info("Received new connection"); + return Mono.just(new RSocket() {}) + .doAfterTerminate(() -> rSockets.put(sendingSocket)); + }) + .lease(spec -> spec.maxPendingRequests(Integer.MAX_VALUE)) + .bindNow(Http3TransportFactory.server("localhost", 7000)); + + logger.info("Server started on port {}", server.address().getPort()); + + // generate stream of fnfs + Flux.generate( + () -> 0L, + (state, sink) -> { + sink.next(state); + return state + 1; + }) + .flatMap( + tick -> { + logger.info("Requesting FireAndForget({})", tick); + + return Mono.fromCallable( + () -> { + RSocket rSocket = rSockets.take(); + rSockets.offer(rSocket); + return rSocket; + }) + .flatMap( + clientRSocket -> + clientRSocket.fireAndForget(ByteBufPayload.create("" + tick))) + .retry(); + }) + .blockLast(); + + server.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/invertmulticlient/RespondingClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/invertmulticlient/RespondingClient.java new file mode 100644 index 000000000..81fbbf71c --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/invertmulticlient/RespondingClient.java @@ -0,0 +1,67 @@ +package io.rsocket.examples.transport.h3.lease.advanced.invertmulticlient; + +import com.netflix.concurrency.limits.limit.VegasLimit; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.examples.transport.h3.lease.advanced.common.LeaseManager; +import io.rsocket.examples.transport.h3.lease.advanced.common.LimitBasedLeaseSender; +import io.rsocket.examples.transport.h3.lease.advanced.controller.TasksHandlingRSocket; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +public class RespondingClient { + private static final Logger logger = LoggerFactory.getLogger(RespondingClient.class); + + public static final int PROCESSING_TASK_TIME = 500; + public static final int CONCURRENT_WORKERS_COUNT = 1; + public static final int QUEUE_CAPACITY = 50; + + public static void main(String[] args) { + // Queue for incoming messages represented as Flux + // Imagine that every fireAndForget that is pushed is processed by a worker + BlockingQueue tasksQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); + + ThreadPoolExecutor threadPoolExecutor = + new ThreadPoolExecutor(1, CONCURRENT_WORKERS_COUNT, 1, TimeUnit.MINUTES, tasksQueue); + + Scheduler workScheduler = Schedulers.fromExecutorService(threadPoolExecutor); + + LeaseManager periodicLeaseSender = + new LeaseManager(CONCURRENT_WORKERS_COUNT, PROCESSING_TASK_TIME); + + Disposable.Composite disposable = Disposables.composite(); + RSocket clientRSocket = + RSocketConnector.create() + .acceptor( + SocketAcceptor.with( + new TasksHandlingRSocket(disposable, workScheduler, PROCESSING_TASK_TIME))) + .lease( + (config) -> + config.sender( + new LimitBasedLeaseSender( + UUID.randomUUID().toString(), + periodicLeaseSender, + VegasLimit.newBuilder() + .initialLimit(CONCURRENT_WORKERS_COUNT) + .maxConcurrency(QUEUE_CAPACITY) + .build()))) + .connect(Http3TransportFactory.client("localhost", 7000)) + .block(); + + Objects.requireNonNull(clientRSocket); + disposable.add(clientRSocket); + clientRSocket.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/multiclient/README.MD b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/multiclient/README.MD new file mode 100644 index 000000000..e69de29bb diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/multiclient/RequestingClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/multiclient/RequestingClient.java new file mode 100644 index 000000000..a02b24773 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/multiclient/RequestingClient.java @@ -0,0 +1,41 @@ +package io.rsocket.examples.transport.h3.lease.advanced.multiclient; + +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.util.ByteBufPayload; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +public class RequestingClient { + private static final Logger logger = LoggerFactory.getLogger(RequestingClient.class); + + public static void main(String[] args) { + + RSocket clientRSocket = + RSocketConnector.create() + .lease() + .connect(Http3TransportFactory.client("localhost", 7000)) + .block(); + + Objects.requireNonNull(clientRSocket); + + // generate stream of fnfs + Flux.generate( + () -> 0L, + (state, sink) -> { + sink.next(state); + return state + 1; + }) + .concatMap( + tick -> { + logger.info("Requesting FireAndForget({})", tick); + return clientRSocket.fireAndForget(ByteBufPayload.create("" + tick)); + }) + .blockLast(); + + clientRSocket.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/multiclient/RespondingServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/multiclient/RespondingServer.java new file mode 100644 index 000000000..9abdf56be --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/advanced/multiclient/RespondingServer.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.h3.lease.advanced.multiclient; + +import com.netflix.concurrency.limits.limit.VegasLimit; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.examples.transport.h3.lease.advanced.common.LeaseManager; +import io.rsocket.examples.transport.h3.lease.advanced.common.LimitBasedLeaseSender; +import io.rsocket.examples.transport.h3.lease.advanced.controller.TasksHandlingRSocket; +import io.rsocket.transport.netty.server.CloseableChannel; +import java.util.UUID; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +public class RespondingServer { + + private static final Logger logger = LoggerFactory.getLogger(RespondingServer.class); + + public static final int TASK_PROCESSING_TIME = 500; + public static final int CONCURRENT_WORKERS_COUNT = 1; + public static final int QUEUE_CAPACITY = 50; + + public static void main(String[] args) { + // Queue for incoming messages represented as Flux + // Imagine that every fireAndForget that is pushed is processed by a worker + BlockingQueue tasksQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); + + ThreadPoolExecutor threadPoolExecutor = + new ThreadPoolExecutor(1, CONCURRENT_WORKERS_COUNT, 1, TimeUnit.MINUTES, tasksQueue); + + Scheduler workScheduler = Schedulers.fromExecutorService(threadPoolExecutor); + + LeaseManager leaseManager = new LeaseManager(CONCURRENT_WORKERS_COUNT, TASK_PROCESSING_TIME); + + Disposable.Composite disposable = Disposables.composite(); + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.with( + new TasksHandlingRSocket(disposable, workScheduler, TASK_PROCESSING_TIME))) + .lease( + (config) -> + config.sender( + new LimitBasedLeaseSender( + UUID.randomUUID().toString(), + leaseManager, + VegasLimit.newBuilder() + .initialLimit(CONCURRENT_WORKERS_COUNT) + .maxConcurrency(QUEUE_CAPACITY) + .build()))) + .bindNow(Http3TransportFactory.server("localhost", 7000)); + + disposable.add(server); + + logger.info("Server started on port {}", server.address().getPort()); + server.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/simple/LeaseExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/simple/LeaseExample.java new file mode 100644 index 000000000..defd8abe3 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/lease/simple/LeaseExample.java @@ -0,0 +1,159 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.h3.lease.simple; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.lease.Lease; +import io.rsocket.lease.LeaseSender; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.util.ByteBufPayload; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class LeaseExample { + + private static final Logger logger = LoggerFactory.getLogger(LeaseExample.class); + + private static final String SERVER_TAG = "server"; + private static final String CLIENT_TAG = "client"; + + public static void main(String[] args) { + // Queue for incoming messages represented as Flux + // Imagine that every fireAndForget that is pushed is processed by a worker + + int queueCapacity = 50; + BlockingQueue messagesQueue = new ArrayBlockingQueue<>(queueCapacity); + + // emulating a worker that process data from the queue + Thread workerThread = + new Thread( + () -> { + try { + while (!Thread.currentThread().isInterrupted()) { + String message = messagesQueue.take(); + logger.info("Process message {}", message); + Thread.sleep(500); // emulating processing + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + workerThread.start(); + + CloseableChannel server = + RSocketServer.create( + (setup, sendingSocket) -> + Mono.just( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + // add element. if overflows errors and terminates execution + // specifically to show that lease can limit rate of fnf requests in + // that example + try { + if (!messagesQueue.offer(payload.getDataUtf8())) { + logger.error("Queue has been overflowed. Terminating execution"); + sendingSocket.dispose(); + workerThread.interrupt(); + } + } finally { + payload.release(); + } + return Mono.empty(); + } + })) + .lease(leases -> leases.sender(new LeaseCalculator(SERVER_TAG, messagesQueue))) + .bindNow(Http3TransportFactory.server("localhost", 7000)); + + RSocket clientRSocket = + RSocketConnector.create() + .lease((config) -> config.maxPendingRequests(1)) + .connect(Http3TransportFactory.client(server.address())) + .block(); + + Objects.requireNonNull(clientRSocket); + + // generate stream of fnfs + Flux.generate( + () -> 0L, + (state, sink) -> { + sink.next(state); + return state + 1; + }) + // here we wait for the first lease for the responder side and start execution + // on if there is allowance + .concatMap( + tick -> { + logger.info("Requesting FireAndForget({})", tick); + return clientRSocket.fireAndForget(ByteBufPayload.create("" + tick)); + }) + .blockLast(); + + clientRSocket.onClose().block(); + server.dispose(); + } + + /** + * This is a class responsible for making decision on whether Responder is ready to receive new + * FireAndForget or not base in the number of messages enqueued.
+ * In the nutshell this is responder-side rate-limiter logic which is created for every new + * connection.
+ * In real-world projects this class has to issue leases based on real metrics + */ + private static class LeaseCalculator implements LeaseSender { + final String tag; + final BlockingQueue queue; + + public LeaseCalculator(String tag, BlockingQueue queue) { + this.tag = tag; + this.queue = queue; + } + + @Override + public Flux send() { + Duration ttlDuration = Duration.ofSeconds(5); + // The interval function is used only for the demo purpose and should not be + // considered as the way to issue leases. + // For advanced RateLimiting with Leasing + // consider adopting https://github.com/Netflix/concurrency-limits#server-limiter + return Flux.interval(Duration.ZERO, ttlDuration.dividedBy(2)) + .handle( + (__, sink) -> { + // put queue.remainingCapacity() + 1 here if you want to observe that app is + // terminated because of the queue overflowing + int requests = queue.remainingCapacity(); + + // reissue new lease only if queue has remaining capacity to + // accept more requests + if (requests > 0) { + sink.next(Lease.create(ttlDuration, requests)); + } + }); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/loadbalancer/RoundRobinRSocketLoadbalancerExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/loadbalancer/RoundRobinRSocketLoadbalancerExample.java new file mode 100644 index 000000000..0a86695fe --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/loadbalancer/RoundRobinRSocketLoadbalancerExample.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.h3.loadbalancer; + +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketClient; +import io.rsocket.core.RSocketServer; +import io.rsocket.loadbalance.LoadbalanceRSocketClient; +import io.rsocket.loadbalance.LoadbalanceTarget; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class RoundRobinRSocketLoadbalancerExample { + + public static void main(String[] args) { + CloseableChannel server1 = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + p -> { + System.out.println("Server 1 got fnf " + p.getDataUtf8()); + return Mono.just(DefaultPayload.create("Server 1 response")) + .delayElement(Duration.ofMillis(100)); + })) + .bindNow(Http3TransportFactory.server(8080)); + + CloseableChannel server2 = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + p -> { + System.out.println("Server 2 got fnf " + p.getDataUtf8()); + return Mono.just(DefaultPayload.create("Server 2 response")) + .delayElement(Duration.ofMillis(100)); + })) + .bindNow(Http3TransportFactory.server(8081)); + + CloseableChannel server3 = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + p -> { + System.out.println("Server 3 got fnf " + p.getDataUtf8()); + return Mono.just(DefaultPayload.create("Server 3 response")) + .delayElement(Duration.ofMillis(100)); + })) + .bindNow(Http3TransportFactory.server(8082)); + + LoadbalanceTarget target8080 = LoadbalanceTarget.from("8080", Http3TransportFactory.client(8080)); + LoadbalanceTarget target8081 = LoadbalanceTarget.from("8081", Http3TransportFactory.client(8081)); + LoadbalanceTarget target8082 = LoadbalanceTarget.from("8082", Http3TransportFactory.client(8082)); + + Flux> producer = + Flux.interval(Duration.ofSeconds(5)) + .log() + .map( + i -> { + int val = i.intValue(); + switch (val) { + case 0: + return Collections.emptyList(); + case 1: + return Collections.singletonList(target8080); + case 2: + return Arrays.asList(target8080, target8081); + case 3: + return Arrays.asList(target8080, target8082); + case 4: + return Arrays.asList(target8081, target8082); + case 5: + return Arrays.asList(target8080, target8081, target8082); + case 6: + return Collections.emptyList(); + case 7: + return Collections.emptyList(); + default: + return Arrays.asList(target8080, target8081, target8082); + } + }); + + RSocketClient rSocketClient = + LoadbalanceRSocketClient.builder(producer).roundRobinLoadbalanceStrategy().build(); + + for (int i = 0; i < 10000; i++) { + try { + rSocketClient.requestResponse(Mono.just(DefaultPayload.create("test" + i))).block(); + } catch (Throwable t) { + // no ops + } + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/CompositeMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/CompositeMetadataExample.java new file mode 100644 index 000000000..9bac418ee --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/CompositeMetadataExample.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015-Present the original author or 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.rsocket.examples.transport.h3.metadata.routing; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.metadata.CompositeMetadata; +import io.rsocket.metadata.CompositeMetadataCodec; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TaggingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.util.ByteBufPayload; +import java.util.Collections; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public class CompositeMetadataExample { + static final Logger logger = LoggerFactory.getLogger(CompositeMetadataExample.class); + + public static void main(String[] args) { + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); + + logger.info("Received RequestResponse[route={}]", route); + + payload.release(); + + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } + + return Mono.error(new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(Http3TransportFactory.server("localhost", 7000)); + + RSocket socket = + RSocketConnector.create() + // here we specify that every metadata payload will be encoded using + // CompositeMetadata layout as specified in the following subspec + // https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md + .metadataMimeType(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString()) + .connect(Http3TransportFactory.client("localhost", 7000)) + .block(); + + final ByteBuf routeMetadata = + TaggingMetadataCodec.createTaggingContent( + ByteBufAllocator.DEFAULT, Collections.singletonList("my.test.route")); + final CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataCodec.encodeAndAddMetadata( + compositeMetadata, + ByteBufAllocator.DEFAULT, + WellKnownMimeType.MESSAGE_RSOCKET_ROUTING, + routeMetadata); + + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), compositeMetadata)) + .log() + .block(); + } + + static String decodeRoute(ByteBuf metadata) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(metadata, false); + + for (CompositeMetadata.Entry metadatum : compositeMetadata) { + if (Objects.requireNonNull(metadatum.getMimeType()) + .equals(WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString())) { + return new RoutingMetadata(metadatum.getContent()).iterator().next(); + } + } + + return null; + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/RoutingMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/RoutingMetadataExample.java new file mode 100644 index 000000000..31df59fe8 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/RoutingMetadataExample.java @@ -0,0 +1,82 @@ +/* + * Copyright 2015-Present the original author or 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.rsocket.examples.transport.h3.metadata.routing; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TaggingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.util.ByteBufPayload; +import java.util.Collections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public class RoutingMetadataExample { + static final Logger logger = LoggerFactory.getLogger(RoutingMetadataExample.class); + + public static void main(String[] args) { + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); + + logger.info("Received RequestResponse[route={}]", route); + + payload.release(); + + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } + + return Mono.error(new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(Http3TransportFactory.server("localhost", 7000)); + + RSocket socket = + RSocketConnector.create() + // here we specify that route will be encoded using + // Routing&Tagging Metadata layout specified at this + // subspec https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md + .metadataMimeType(WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()) + .connect(Http3TransportFactory.client("localhost", 7000)) + .block(); + + final ByteBuf routeMetadata = + TaggingMetadataCodec.createTaggingContent( + ByteBufAllocator.DEFAULT, Collections.singletonList("my.test.route")); + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), routeMetadata)) + .log() + .block(); + } + + static String decodeRoute(ByteBuf metadata) { + final RoutingMetadata routingMetadata = new RoutingMetadata(metadata); + + return routingMetadata.iterator().next(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/plugins/LimitRateInterceptorExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/plugins/LimitRateInterceptorExample.java new file mode 100644 index 000000000..f89de7d2b --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/plugins/LimitRateInterceptorExample.java @@ -0,0 +1,82 @@ +package io.rsocket.examples.transport.h3.plugins; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.plugins.LimitRateInterceptor; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +public class LimitRateInterceptorExample { + + private static final Logger logger = LoggerFactory.getLogger(LimitRateInterceptorExample.class); + + public static void main(String[] args) { + RSocketServer.create( + SocketAcceptor.with( + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + return Flux.interval(Duration.ofMillis(100)) + .doOnRequest( + e -> logger.debug("Server publisher receives request for " + e)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .doOnRequest( + e -> logger.debug("Server publisher receives request for " + e)); + } + })) + .interceptors(registry -> registry.forResponder(LimitRateInterceptor.forResponder(64))) + .bindNow(Http3TransportFactory.server("localhost", 7000)); + + RSocket socket = + RSocketConnector.create() + .interceptors(registry -> registry.forRequester(LimitRateInterceptor.forRequester(64))) + .connect(Http3TransportFactory.client("localhost", 7000)) + .block(); + + logger.debug( + "\n\nStart of requestStream interaction\n" + "----------------------------------\n"); + + socket + .requestStream(DefaultPayload.create("Hello")) + .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); + + logger.debug( + "\n\nStart of requestChannel interaction\n" + "-----------------------------------\n"); + + socket + .requestChannel( + Flux.generate( + () -> 1L, + (s, sink) -> { + sink.next(DefaultPayload.create("Next " + s)); + return ++s; + }) + .doOnRequest(e -> logger.debug("Client publisher receives request for " + e))) + .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .doFinally(signalType -> socket.dispose()) + .then() + .block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/requestresponse/HelloWorldClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/requestresponse/HelloWorldClient.java new file mode 100644 index 000000000..02e3839e5 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/requestresponse/HelloWorldClient.java @@ -0,0 +1,68 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.h3.requestresponse; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.util.DefaultPayload; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public final class HelloWorldClient { + + private static final Logger logger = LoggerFactory.getLogger(HelloWorldClient.class); + + public static void main(String[] args) { + + RSocket rsocket = + new RSocket() { + boolean fail = true; + + @Override + public Mono requestResponse(Payload p) { + if (fail) { + fail = false; + return Mono.error(new Throwable("Simulated error")); + } else { + return Mono.just(p); + } + } + }; + + RSocketServer.create(SocketAcceptor.with(rsocket)) + .bindNow(Http3TransportFactory.server("localhost", 7000)); + + RSocket socket = + RSocketConnector.connectWith(Http3TransportFactory.client("localhost", 7000)).block(); + + for (int i = 0; i < 3; i++) { + socket + .requestResponse(DefaultPayload.create("Hello")) + .map(Payload::getDataUtf8) + .onErrorReturn("error") + .doOnNext(logger::debug) + .block(); + } + + socket.dispose(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/resume/ResumeFileTransfer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/resume/ResumeFileTransfer.java new file mode 100644 index 000000000..3e621e8c3 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/resume/ResumeFileTransfer.java @@ -0,0 +1,113 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.h3.resume; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.core.Resume; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.examples.transport.support.DisconnectableClientTransport; +import io.rsocket.examples.transport.support.ResumeExampleSupport; +import io.rsocket.examples.transport.support.ResumeFiles; +import io.rsocket.examples.transport.support.ResumeRequest; +import io.rsocket.examples.transport.support.ResumeRequestCodec; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; + +public class ResumeFileTransfer { + private static final String HOST = "localhost"; + private static final int CHUNK_SIZE = 16; + private static final String INPUT_FILE = "lorem.txt"; + private static final String OUTPUT_FILE = "build/lorem_output_h3.txt"; + + /*amount of file chunks requested by subscriber: n, refilled on n/2 of received items*/ + private static final int PREFETCH_WINDOW_SIZE = 4; + private static final Duration FILE_CHUNK_DELAY = Duration.ofMillis(50); + private static final Duration DISCONNECT_INTERVAL = Duration.ofSeconds(1); + private static final Duration DISCONNECT_COOLDOWN = Duration.ofMillis(500); + private static final Duration RESUME_RETRY_DELAY = Duration.ofMillis(250); + private static final Logger logger = LoggerFactory.getLogger(ResumeFileTransfer.class); + + public static void main(String[] args) { + AtomicBoolean shuttingDown = new AtomicBoolean(); + + try (ResumeExampleSupport.HookHandle hookHandle = + ResumeExampleSupport.suppressExpectedShutdownErrors(shuttingDown, logger)) { + hookHandle.keep(); + Resume resume = ResumeExampleSupport.resume(RESUME_RETRY_DELAY, shuttingDown, logger); + + ResumeRequestCodec codec = new ResumeRequestCodec(); + + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> { + ResumeRequest request = codec.decode(payload); + payload.release(); + + Flux ticks = Flux.interval(FILE_CHUNK_DELAY).onBackpressureDrop(); + + return ResumeFiles.fileSource(request.getFileName(), request.getChunkSize()) + .map(DefaultPayload::create) + .zipWith(ticks, (p, tick) -> p) + .log("server"); + })) + .resume(resume) + .bindNow(Http3TransportFactory.server(HOST, 0)); + + DisconnectableClientTransport clientTransport = + new DisconnectableClientTransport( + Http3TransportFactory.client(HOST, server.address().getPort())); + + RSocket client = + RSocketConnector.create() + .resume(resume) + .connect(clientTransport) + .block(); + + Disposable disconnectSimulation = + ResumeExampleSupport.intervalDisconnectSimulation( + DISCONNECT_INTERVAL, + 2, + clientTransport, + DISCONNECT_COOLDOWN, + shuttingDown, + logger); + + try { + ResumeExampleSupport.runRequestStream( + client, + codec.encode(new ResumeRequest(CHUNK_SIZE, INPUT_FILE)), + ResumeFiles.fileSink(OUTPUT_FILE, PREFETCH_WINDOW_SIZE, () -> {}, __ -> {})) + .block(); + } finally { + disconnectSimulation.dispose(); + ResumeExampleSupport.shutdown(shuttingDown, client, clientTransport, server, logger); + } + } + } + +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/resume/readme.md b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/resume/readme.md new file mode 100644 index 000000000..ffbb38859 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/resume/readme.md @@ -0,0 +1,22 @@ +1. Start `ResumeFileTransfer.main` + +2. The example simulates two short transport disconnects in-process and resumes the stream each time + +`ResumeFileTransfer` output is as follows + +``` +Received file chunk: 7. Total size: 112 +Received file chunk: 8. Total size: 128 +Received file chunk: 9. Total size: 144 +Received file chunk: 10. Total size: 160 +Disconnected. Trying to resume... +Disconnected. Trying to resume... +Received file chunk: 11. Total size: 176 +Received file chunk: 12. Total size: 192 +Received file chunk: 13. Total size: 208 +Received file chunk: 14. Total size: 224 +Received file chunk: 15. Total size: 240 +Received file chunk: 16. Total size: 256 +``` + +It transfers file from `resources/lorem.txt` to `build/lorem_output_h3.txt` in chunks of 16 bytes every 50 millis diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ClientStreamingToServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ClientStreamingToServer.java new file mode 100644 index 000000000..e0d84dcd2 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ClientStreamingToServer.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.h3.stream; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +public final class ClientStreamingToServer { + + private static final Logger logger = LoggerFactory.getLogger(ClientStreamingToServer.class); + + public static void main(String[] args) throws InterruptedException { + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> + Flux.interval(Duration.ofMillis(100)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)))) + .bindNow(Http3TransportFactory.server("localhost", 7000)); + + RSocket socket = + RSocketConnector.create() + .setupPayload(DefaultPayload.create("test", "test")) + .connect(Http3TransportFactory.client("localhost", 7000)) + .block(); + + final Payload payload = DefaultPayload.create("Hello"); + socket + .requestStream(payload) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .doFinally(signalType -> socket.dispose()) + .then() + .block(); + + Thread.sleep(1000000); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ServerStreamingToClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ServerStreamingToClient.java new file mode 100644 index 000000000..33a56494d --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ServerStreamingToClient.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.h3.stream; + +import static io.rsocket.SocketAcceptor.forRequestStream; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public final class ServerStreamingToClient { + + public static void main(String[] args) { + + RSocketServer.create( + (setup, rsocket) -> { + rsocket + .requestStream(DefaultPayload.create("Hello-Bidi")) + .map(Payload::getDataUtf8) + .log() + .subscribe(); + + return Mono.just(new RSocket() {}); + }) + .bindNow(Http3TransportFactory.server("localhost", 7000)); + + RSocket rsocket = + RSocketConnector.create() + .acceptor( + forRequestStream( + payload -> + Flux.interval(Duration.ofSeconds(1)) + .map(aLong -> DefaultPayload.create("Bi-di Response => " + aLong)))) + .connect(Http3TransportFactory.client("localhost", 7000)) + .block(); + + rsocket.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/QuicTransportFactory.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/QuicTransportFactory.java new file mode 100644 index 000000000..9f9b7247e --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/QuicTransportFactory.java @@ -0,0 +1,99 @@ +/* + * Copyright 2015-2026 the original author or 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.rsocket.examples.transport.quic; + +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.netty.incubator.codec.quic.QuicSslContext; +import io.netty.incubator.codec.quic.QuicSslContextBuilder; +import io.rsocket.transport.netty.QuicTransportConfig; +import io.rsocket.transport.netty.client.QuicClientTransport; +import io.rsocket.transport.netty.server.QuicServerTransport; +import java.net.InetSocketAddress; + +public final class QuicTransportFactory { + + private static final String DEFAULT_HOST = "127.0.0.1"; + private static final SelfSignedCertificate CERTIFICATE = createCertificate(); + private static final QuicTransportConfig SERVER_CONFIG = createServerConfig(); + private static final QuicTransportConfig CLIENT_CONFIG = createClientConfig(); + + static { + Runtime.getRuntime().addShutdownHook(new Thread(CERTIFICATE::delete)); + } + + private QuicTransportFactory() {} + + public static QuicClientTransport client(int port) { + return client(DEFAULT_HOST, port); + } + + public static QuicClientTransport client(String host, int port) { + return QuicClientTransport.create(host, port).config(CLIENT_CONFIG); + } + + public static QuicClientTransport client(InetSocketAddress address) { + return client(address.getHostString(), address.getPort()); + } + + public static QuicServerTransport server(int port) { + return server(DEFAULT_HOST, port); + } + + public static QuicServerTransport server(String host, int port) { + return QuicServerTransport.create(host, port).config(SERVER_CONFIG); + } + + private static SelfSignedCertificate createCertificate() { + try { + return new SelfSignedCertificate(); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + private static QuicTransportConfig createServerConfig() { + try { + QuicSslContext serverSslContext = + QuicSslContextBuilder.forServer( + CERTIFICATE.privateKey(), null, CERTIFICATE.certificate()) + .applicationProtocols("rsocket") + .build(); + + return QuicTransportConfig.builder().sslContext(serverSslContext).build(); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } + + private static QuicTransportConfig createClientConfig() { + try { + QuicSslContext clientSslContext = + QuicSslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE) + .applicationProtocols("rsocket") + .build(); + + return QuicTransportConfig.builder() + .sslContext(clientSslContext) + .validateCertificates(false) + .build(); + } catch (Exception e) { + throw new ExceptionInInitializerError(e); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/channel/ChannelEchoClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/channel/ChannelEchoClient.java new file mode 100644 index 000000000..94b8bb5e3 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/channel/ChannelEchoClient.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.quic.channel; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +public final class ChannelEchoClient { + + private static final Logger logger = LoggerFactory.getLogger(ChannelEchoClient.class); + + public static void main(String[] args) { + + SocketAcceptor echoAcceptor = + SocketAcceptor.forRequestChannel( + payloads -> + Flux.from(payloads) + .map(Payload::getDataUtf8) + .map(s -> "Echo: " + s) + .map(DefaultPayload::create)); + + RSocketServer.create(echoAcceptor).bindNow(QuicTransportFactory.server("localhost", 7000)); + + RSocket socket = + RSocketConnector.connectWith(QuicTransportFactory.client("localhost", 7000)).block(); + + socket + .requestChannel( + Flux.interval(Duration.ofMillis(1000)).map(i -> DefaultPayload.create("Hello"))) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .doFinally(signalType -> socket.dispose()) + .then() + .block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/client/RSocketClientExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/client/RSocketClientExample.java new file mode 100644 index 000000000..66ff4fb73 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/client/RSocketClientExample.java @@ -0,0 +1,54 @@ +package io.rsocket.examples.transport.quic.client; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketClient; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +public class RSocketClientExample { + static final Logger logger = LoggerFactory.getLogger(RSocketClientExample.class); + + public static void main(String[] args) { + + RSocketServer.create( + SocketAcceptor.forRequestResponse( + p -> { + String data = p.getDataUtf8(); + logger.info("Received request data {}", data); + + Payload responsePayload = DefaultPayload.create("Echo: " + data); + p.release(); + + return Mono.just(responsePayload); + })) + .bind(QuicTransportFactory.server("localhost", 7000)) + .delaySubscription(Duration.ofSeconds(5)) + .doOnNext(cc -> logger.info("Server started on the address : {}", cc.address())) + .block(); + + Mono source = + RSocketConnector.create() + .reconnect(Retry.backoff(50, Duration.ofMillis(500))) + .connect(QuicTransportFactory.client("localhost", 7000)); + + RSocketClient.from(source) + .requestResponse(Mono.just(DefaultPayload.create("Test Request"))) + .doOnSubscribe(s -> logger.info("Executing Request")) + .doOnNext( + d -> { + logger.info("Received response data {}", d.getDataUtf8()); + d.release(); + }) + .repeat(10) + .blockLast(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/fnf/TaskProcessingWithServerSideNotificationsExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/fnf/TaskProcessingWithServerSideNotificationsExample.java new file mode 100644 index 000000000..c194dc274 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/fnf/TaskProcessingWithServerSideNotificationsExample.java @@ -0,0 +1,236 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.quic.fnf; + +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ThreadLocalRandom; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Sinks; +import reactor.util.concurrent.Queues; + +/** + * An example of long-running tasks processing (a.k.a Kafka style) where a client submits tasks over + * request `FireAndForget` and then receives results over the same method but on it is own side. + * + *

This example shows a case when the client may disappear, however, another a client can connect + * again and receive undelivered completed tasks remaining for the previous one. + */ +public class TaskProcessingWithServerSideNotificationsExample { + + public static void main(String[] args) throws InterruptedException { + Sinks.Many tasksProcessor = + Sinks.many().unicast().onBackpressureBuffer(Queues.unboundedMultiproducer().get()); + ConcurrentMap> idToCompletedTasksMap = new ConcurrentHashMap<>(); + ConcurrentMap idToRSocketMap = new ConcurrentHashMap<>(); + BackgroundWorker backgroundWorker = + new BackgroundWorker(tasksProcessor.asFlux(), idToCompletedTasksMap, idToRSocketMap); + + RSocketServer.create(new TasksAcceptor(tasksProcessor, idToCompletedTasksMap, idToRSocketMap)) + .bindNow(QuicTransportFactory.server(9991)); + + Logger logger = LoggerFactory.getLogger("RSocket.Client.ID[Test]"); + + Mono rSocketMono = + RSocketConnector.create() + .setupPayload(DefaultPayload.create("Test")) + .acceptor( + SocketAcceptor.forFireAndForget( + p -> { + logger.info("Received Processed Task[{}]", p.getDataUtf8()); + p.release(); + return Mono.empty(); + })) + .connect(QuicTransportFactory.client(9991)); + + RSocket rSocketRequester1 = rSocketMono.block(); + + for (int i = 0; i < 10; i++) { + rSocketRequester1.fireAndForget(DefaultPayload.create("task" + i)).block(); + } + + Thread.sleep(4000); + + rSocketRequester1.dispose(); + logger.info("Disposed"); + + Thread.sleep(4000); + + RSocket rSocketRequester2 = rSocketMono.block(); + + logger.info("Reconnected"); + + Thread.sleep(10000); + } + + static class BackgroundWorker extends BaseSubscriber { + final ConcurrentMap> idToCompletedTasksMap; + final ConcurrentMap idToRSocketMap; + + BackgroundWorker( + Flux taskProducer, + ConcurrentMap> idToCompletedTasksMap, + ConcurrentMap idToRSocketMap) { + + this.idToCompletedTasksMap = idToCompletedTasksMap; + this.idToRSocketMap = idToRSocketMap; + + // mimic a long running task processing + taskProducer + .concatMap( + t -> + Mono.delay(Duration.ofMillis(ThreadLocalRandom.current().nextInt(200, 2000))) + .thenReturn(t)) + .subscribe(this); + } + + @Override + protected void hookOnNext(Task task) { + BlockingQueue completedTasksQueue = + idToCompletedTasksMap.computeIfAbsent(task.id, __ -> new LinkedBlockingQueue<>()); + + completedTasksQueue.offer(task); + RSocket rSocket = idToRSocketMap.get(task.id); + if (rSocket != null) { + rSocket + .fireAndForget(DefaultPayload.create(task.content)) + .subscribe(null, e -> {}, () -> completedTasksQueue.remove(task)); + } + } + } + + static class TasksAcceptor implements SocketAcceptor { + + static final Logger logger = LoggerFactory.getLogger(TasksAcceptor.class); + + final Sinks.Many tasksToProcess; + final ConcurrentMap> idToCompletedTasksMap; + final ConcurrentMap idToRSocketMap; + + TasksAcceptor( + Sinks.Many tasksToProcess, + ConcurrentMap> idToCompletedTasksMap, + ConcurrentMap idToRSocketMap) { + this.tasksToProcess = tasksToProcess; + this.idToCompletedTasksMap = idToCompletedTasksMap; + this.idToRSocketMap = idToRSocketMap; + } + + @Override + public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) { + String id = setup.getDataUtf8(); + logger.info("Accepting a new client connection with ID {}", id); + // sendingRSocket represents here an RSocket requester to a remote peer + + if (this.idToRSocketMap.compute( + id, (__, old) -> old == null || old.isDisposed() ? sendingSocket : old) + == sendingSocket) { + return Mono.just( + new RSocketTaskHandler(idToRSocketMap, tasksToProcess, id, sendingSocket)) + .doOnSuccess(__ -> checkTasksToDeliver(sendingSocket, id)); + } + + return Mono.error( + new IllegalStateException("There is already a client connected with the same ID")); + } + + private void checkTasksToDeliver(RSocket sendingSocket, String id) { + logger.info("Accepted a new client connection with ID {}. Checking for remaining tasks", id); + BlockingQueue tasksToDeliver = this.idToCompletedTasksMap.get(id); + + if (tasksToDeliver == null || tasksToDeliver.isEmpty()) { + // means nothing yet to send + return; + } + + logger.info("Found remaining tasks to deliver for client {}", id); + + for (; ; ) { + Task task = tasksToDeliver.poll(); + + if (task == null) { + return; + } + + sendingSocket + .fireAndForget(DefaultPayload.create(task.content)) + .subscribe( + null, + e -> { + // offers back a task if it has not been delivered + tasksToDeliver.offer(task); + }); + } + } + + private static class RSocketTaskHandler implements RSocket { + + private final String id; + private final RSocket sendingSocket; + private ConcurrentMap idToRSocketMap; + private Sinks.Many tasksToProcess; + + public RSocketTaskHandler( + ConcurrentMap idToRSocketMap, + Sinks.Many tasksToProcess, + String id, + RSocket sendingSocket) { + this.id = id; + this.sendingSocket = sendingSocket; + this.idToRSocketMap = idToRSocketMap; + this.tasksToProcess = tasksToProcess; + } + + @Override + public Mono fireAndForget(Payload payload) { + logger.info("Received a Task[{}] from Client.ID[{}]", payload.getDataUtf8(), id); + Sinks.EmitResult result = tasksToProcess.tryEmitNext(new Task(id, payload.getDataUtf8())); + payload.release(); + return result.isFailure() ? Mono.error(new Sinks.EmissionException(result)) : Mono.empty(); + } + + @Override + public void dispose() { + idToRSocketMap.remove(id, sendingSocket); + } + } + } + + static class Task { + final String id; + final String content; + + Task(String id, String content) { + this.id = id; + this.content = content; + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/common/LeaseManager.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/common/LeaseManager.java new file mode 100644 index 000000000..5874d2371 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/common/LeaseManager.java @@ -0,0 +1,146 @@ +package io.rsocket.examples.transport.quic.lease.advanced.common; + +import io.rsocket.examples.transport.quic.QuicTransportFactory; + +import java.util.concurrent.BlockingDeque; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +public class LeaseManager implements Runnable { + + static final Logger logger = LoggerFactory.getLogger(LeaseManager.class); + + volatile int activeConnectionsCount; + static final AtomicIntegerFieldUpdater ACTIVE_CONNECTIONS_COUNT = + AtomicIntegerFieldUpdater.newUpdater(LeaseManager.class, "activeConnectionsCount"); + + volatile int stateAndInFlight; + static final AtomicIntegerFieldUpdater STATE_AND_IN_FLIGHT = + AtomicIntegerFieldUpdater.newUpdater(LeaseManager.class, "stateAndInFlight"); + + static final int MASK_PAUSED = 0b1_000_0000_0000_0000_0000_0000_0000_0000; + static final int MASK_IN_FLIGHT = 0b0_111_1111_1111_1111_1111_1111_1111_1111; + + final BlockingDeque sendersQueue = new LinkedBlockingDeque<>(); + final Scheduler worker = Schedulers.newSingle(LeaseManager.class.getName()); + + final int capacity; + final int ttl; + + public LeaseManager(int capacity, int ttl) { + this.capacity = capacity; + this.ttl = ttl; + } + + @Override + public void run() { + try { + LimitBasedLeaseSender leaseSender = sendersQueue.poll(); + + if (leaseSender == null) { + return; + } + + if (leaseSender.isDisposed()) { + logger.debug("Connection[" + leaseSender.connectionId + "]: LeaseSender is Disposed"); + worker.schedule(this); + return; + } + + int limit = leaseSender.limitAlgorithm.getLimit(); + + if (limit == 0) { + throw new IllegalStateException("Limit is 0"); + } + + if (pauseIfNoCapacity()) { + sendersQueue.addFirst(leaseSender); + logger.debug("Pause execution. Not enough capacity"); + return; + } + + leaseSender.sendLease(ttl, limit); + sendersQueue.offer(leaseSender); + + int activeConnections = activeConnectionsCount; + int nextDelay = activeConnections == 0 ? ttl : (ttl / activeConnections); + + logger.debug("Next check happens in " + nextDelay + "ms"); + + worker.schedule(this, nextDelay, TimeUnit.MILLISECONDS); + } catch (Throwable e) { + logger.error("LeaseSender failed to send lease", e); + } + } + + int incrementInFlightAndGet() { + for (; ; ) { + int state = stateAndInFlight; + int paused = state & MASK_PAUSED; + int inFlight = stateAndInFlight & MASK_IN_FLIGHT; + + // assume overflow is impossible due to max concurrency in RSocket it self + int nextInFlight = inFlight + 1; + + if (STATE_AND_IN_FLIGHT.compareAndSet(this, state, nextInFlight | paused)) { + return nextInFlight; + } + } + } + + void decrementInFlight() { + for (; ; ) { + int state = stateAndInFlight; + int paused = state & MASK_PAUSED; + int inFlight = stateAndInFlight & MASK_IN_FLIGHT; + + // assume overflow is impossible due to max concurrency in RSocket it self + int nextInFlight = inFlight - 1; + + if (inFlight == capacity && paused == MASK_PAUSED) { + if (STATE_AND_IN_FLIGHT.compareAndSet(this, state, nextInFlight)) { + logger.debug("Resume execution"); + worker.schedule(this); + return; + } + } else { + if (STATE_AND_IN_FLIGHT.compareAndSet(this, state, nextInFlight | paused)) { + return; + } + } + } + } + + boolean pauseIfNoCapacity() { + int capacity = this.capacity; + for (; ; ) { + int inFlight = stateAndInFlight; + + if (inFlight < capacity) { + return false; + } + + if (STATE_AND_IN_FLIGHT.compareAndSet(this, inFlight, inFlight | MASK_PAUSED)) { + return true; + } + } + } + + void unregister() { + ACTIVE_CONNECTIONS_COUNT.decrementAndGet(this); + } + + void register(LimitBasedLeaseSender sender) { + sendersQueue.offer(sender); + final int activeCount = ACTIVE_CONNECTIONS_COUNT.getAndIncrement(this); + + if (activeCount == 0) { + worker.schedule(this); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/common/LimitBasedLeaseSender.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/common/LimitBasedLeaseSender.java new file mode 100644 index 000000000..b36c7ec15 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/common/LimitBasedLeaseSender.java @@ -0,0 +1,56 @@ +package io.rsocket.examples.transport.quic.lease.advanced.common; + +import io.rsocket.examples.transport.quic.QuicTransportFactory; + +import com.netflix.concurrency.limits.Limit; +import io.rsocket.lease.Lease; +import io.rsocket.lease.TrackingLeaseSender; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Sinks; +import reactor.util.concurrent.Queues; + +public class LimitBasedLeaseSender extends LimitBasedStatsCollector implements TrackingLeaseSender { + + static final Logger logger = LoggerFactory.getLogger(LimitBasedLeaseSender.class); + + final String connectionId; + final Sinks.Many sink = + Sinks.many().unicast().onBackpressureBuffer(Queues.one().get()); + + public LimitBasedLeaseSender( + String connectionId, LeaseManager leaseManager, Limit limitAlgorithm) { + super(leaseManager, limitAlgorithm); + this.connectionId = connectionId; + } + + @Override + public Flux send() { + logger.info("Received new leased Connection[" + connectionId + "]"); + + leaseManager.register(this); + + return sink.asFlux(); + } + + public void sendLease(int ttl, int amount) { + final Lease nextLease = Lease.create(Duration.ofMillis(ttl), amount); + final Sinks.EmitResult result = sink.tryEmitNext(nextLease); + + if (result.isFailure()) { + logger.warn( + "Connection[" + + connectionId + + "]. Issued Lease: [" + + nextLease + + "] was not sent due to " + + result); + } else { + if (logger.isDebugEnabled()) { + logger.debug("To Connection[" + connectionId + "]: Issued Lease: [" + nextLease + "]"); + } + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/common/LimitBasedStatsCollector.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/common/LimitBasedStatsCollector.java new file mode 100644 index 000000000..bf8babff0 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/common/LimitBasedStatsCollector.java @@ -0,0 +1,75 @@ +package io.rsocket.examples.transport.quic.lease.advanced.common; + +import io.rsocket.examples.transport.quic.QuicTransportFactory; + +import com.netflix.concurrency.limits.Limit; +import io.netty.buffer.ByteBuf; +import io.rsocket.frame.FrameType; +import io.rsocket.plugins.RequestInterceptor; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.LongSupplier; +import reactor.util.annotation.Nullable; + +public class LimitBasedStatsCollector extends AtomicBoolean implements RequestInterceptor { + + final LeaseManager leaseManager; + final Limit limitAlgorithm; + + final ConcurrentMap inFlightMap = new ConcurrentHashMap<>(); + final ConcurrentMap timeMap = new ConcurrentHashMap<>(); + + final LongSupplier clock = System::nanoTime; + + public LimitBasedStatsCollector(LeaseManager leaseManager, Limit limitAlgorithm) { + this.leaseManager = leaseManager; + this.limitAlgorithm = limitAlgorithm; + } + + @Override + public void onStart(int streamId, FrameType requestType, @Nullable ByteBuf metadata) { + long startTime = clock.getAsLong(); + + int currentInFlight = leaseManager.incrementInFlightAndGet(); + + inFlightMap.put(streamId, currentInFlight); + timeMap.put(streamId, startTime); + } + + @Override + public void onReject( + Throwable rejectionReason, FrameType requestType, @Nullable ByteBuf metadata) {} + + @Override + public void onTerminate(int streamId, FrameType requestType, @Nullable Throwable t) { + leaseManager.decrementInFlight(); + + Long startTime = timeMap.remove(streamId); + Integer currentInflight = inFlightMap.remove(streamId); + + limitAlgorithm.onSample(startTime, clock.getAsLong() - startTime, currentInflight, t != null); + } + + @Override + public void onCancel(int streamId, FrameType requestType) { + leaseManager.decrementInFlight(); + + Long startTime = timeMap.remove(streamId); + Integer currentInflight = inFlightMap.remove(streamId); + + limitAlgorithm.onSample(startTime, clock.getAsLong() - startTime, currentInflight, true); + } + + @Override + public boolean isDisposed() { + return get(); + } + + @Override + public void dispose() { + if (!getAndSet(true)) { + leaseManager.unregister(); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/controller/Task.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/controller/Task.java new file mode 100644 index 000000000..9803255ea --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/controller/Task.java @@ -0,0 +1,29 @@ +package io.rsocket.examples.transport.quic.lease.advanced.controller; + +import io.rsocket.examples.transport.quic.QuicTransportFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +// emulating a worker that process data from the queue +public class Task implements Runnable { + private static final Logger logger = LoggerFactory.getLogger(Task.class); + + final String message; + final int processingTime; + + Task(String message, int processingTime) { + this.message = message; + this.processingTime = processingTime; + } + + @Override + public void run() { + logger.info("Processing Task[{}]", message); + try { + Thread.sleep(processingTime); // emulating processing + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/controller/TasksHandlingRSocket.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/controller/TasksHandlingRSocket.java new file mode 100644 index 000000000..307f821e5 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/controller/TasksHandlingRSocket.java @@ -0,0 +1,46 @@ +package io.rsocket.examples.transport.quic.lease.advanced.controller; + +import io.rsocket.examples.transport.quic.QuicTransportFactory; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Scheduler; + +public class TasksHandlingRSocket implements RSocket { + + private static final Logger logger = LoggerFactory.getLogger(TasksHandlingRSocket.class); + + final Disposable terminatable; + final Scheduler workScheduler; + final int processingTime; + + public TasksHandlingRSocket(Disposable terminatable, Scheduler scheduler, int processingTime) { + this.terminatable = terminatable; + this.workScheduler = scheduler; + this.processingTime = processingTime; + } + + @Override + public Mono fireAndForget(Payload payload) { + + // specifically to show that lease can limit rate of fnf requests in + // that example + String message = payload.getDataUtf8(); + payload.release(); + + return Mono.fromRunnable(new Task(message, processingTime)) + // schedule task on specific, limited in size scheduler + .subscribeOn(workScheduler) + // if errors - terminates server + .doOnError( + t -> { + logger.error("Queue has been overflowed. Terminating server"); + terminatable.dispose(); + System.exit(9); + }); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/invertmulticlient/README.MD b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/invertmulticlient/README.MD new file mode 100644 index 000000000..e69de29bb diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/invertmulticlient/RequestingServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/invertmulticlient/RequestingServer.java new file mode 100644 index 000000000..e2aad6db9 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/invertmulticlient/RequestingServer.java @@ -0,0 +1,78 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.quic.lease.advanced.invertmulticlient; + +import io.rsocket.RSocket; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.util.ByteBufPayload; +import java.util.Comparator; +import java.util.concurrent.PriorityBlockingQueue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class RequestingServer { + + private static final Logger logger = LoggerFactory.getLogger(RequestingServer.class); + + public static void main(String[] args) { + PriorityBlockingQueue rSockets = + new PriorityBlockingQueue<>( + 16, Comparator.comparingDouble(RSocket::availability).reversed()); + + CloseableChannel server = + RSocketServer.create( + (setup, sendingSocket) -> { + logger.info("Received new connection"); + return Mono.just(new RSocket() {}) + .doAfterTerminate(() -> rSockets.put(sendingSocket)); + }) + .lease(spec -> spec.maxPendingRequests(Integer.MAX_VALUE)) + .bindNow(QuicTransportFactory.server("localhost", 7000)); + + logger.info("Server started on port {}", server.address().getPort()); + + // generate stream of fnfs + Flux.generate( + () -> 0L, + (state, sink) -> { + sink.next(state); + return state + 1; + }) + .flatMap( + tick -> { + logger.info("Requesting FireAndForget({})", tick); + + return Mono.fromCallable( + () -> { + RSocket rSocket = rSockets.take(); + rSockets.offer(rSocket); + return rSocket; + }) + .flatMap( + clientRSocket -> + clientRSocket.fireAndForget(ByteBufPayload.create("" + tick))) + .retry(); + }) + .blockLast(); + + server.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/invertmulticlient/RespondingClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/invertmulticlient/RespondingClient.java new file mode 100644 index 000000000..5bf9775e6 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/invertmulticlient/RespondingClient.java @@ -0,0 +1,67 @@ +package io.rsocket.examples.transport.quic.lease.advanced.invertmulticlient; + +import com.netflix.concurrency.limits.limit.VegasLimit; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.examples.transport.quic.lease.advanced.common.LeaseManager; +import io.rsocket.examples.transport.quic.lease.advanced.common.LimitBasedLeaseSender; +import io.rsocket.examples.transport.quic.lease.advanced.controller.TasksHandlingRSocket; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +public class RespondingClient { + private static final Logger logger = LoggerFactory.getLogger(RespondingClient.class); + + public static final int PROCESSING_TASK_TIME = 500; + public static final int CONCURRENT_WORKERS_COUNT = 1; + public static final int QUEUE_CAPACITY = 50; + + public static void main(String[] args) { + // Queue for incoming messages represented as Flux + // Imagine that every fireAndForget that is pushed is processed by a worker + BlockingQueue tasksQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); + + ThreadPoolExecutor threadPoolExecutor = + new ThreadPoolExecutor(1, CONCURRENT_WORKERS_COUNT, 1, TimeUnit.MINUTES, tasksQueue); + + Scheduler workScheduler = Schedulers.fromExecutorService(threadPoolExecutor); + + LeaseManager periodicLeaseSender = + new LeaseManager(CONCURRENT_WORKERS_COUNT, PROCESSING_TASK_TIME); + + Disposable.Composite disposable = Disposables.composite(); + RSocket clientRSocket = + RSocketConnector.create() + .acceptor( + SocketAcceptor.with( + new TasksHandlingRSocket(disposable, workScheduler, PROCESSING_TASK_TIME))) + .lease( + (config) -> + config.sender( + new LimitBasedLeaseSender( + UUID.randomUUID().toString(), + periodicLeaseSender, + VegasLimit.newBuilder() + .initialLimit(CONCURRENT_WORKERS_COUNT) + .maxConcurrency(QUEUE_CAPACITY) + .build()))) + .connect(QuicTransportFactory.client("localhost", 7000)) + .block(); + + Objects.requireNonNull(clientRSocket); + disposable.add(clientRSocket); + clientRSocket.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/multiclient/README.MD b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/multiclient/README.MD new file mode 100644 index 000000000..e69de29bb diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/multiclient/RequestingClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/multiclient/RequestingClient.java new file mode 100644 index 000000000..9688ea879 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/multiclient/RequestingClient.java @@ -0,0 +1,41 @@ +package io.rsocket.examples.transport.quic.lease.advanced.multiclient; + +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.util.ByteBufPayload; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +public class RequestingClient { + private static final Logger logger = LoggerFactory.getLogger(RequestingClient.class); + + public static void main(String[] args) { + + RSocket clientRSocket = + RSocketConnector.create() + .lease() + .connect(QuicTransportFactory.client("localhost", 7000)) + .block(); + + Objects.requireNonNull(clientRSocket); + + // generate stream of fnfs + Flux.generate( + () -> 0L, + (state, sink) -> { + sink.next(state); + return state + 1; + }) + .concatMap( + tick -> { + logger.info("Requesting FireAndForget({})", tick); + return clientRSocket.fireAndForget(ByteBufPayload.create("" + tick)); + }) + .blockLast(); + + clientRSocket.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/multiclient/RespondingServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/multiclient/RespondingServer.java new file mode 100644 index 000000000..a0cde61ad --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/advanced/multiclient/RespondingServer.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.quic.lease.advanced.multiclient; + +import com.netflix.concurrency.limits.limit.VegasLimit; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.examples.transport.quic.lease.advanced.common.LeaseManager; +import io.rsocket.examples.transport.quic.lease.advanced.common.LimitBasedLeaseSender; +import io.rsocket.examples.transport.quic.lease.advanced.controller.TasksHandlingRSocket; +import io.rsocket.transport.netty.server.CloseableChannel; +import java.util.UUID; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; + +public class RespondingServer { + + private static final Logger logger = LoggerFactory.getLogger(RespondingServer.class); + + public static final int TASK_PROCESSING_TIME = 500; + public static final int CONCURRENT_WORKERS_COUNT = 1; + public static final int QUEUE_CAPACITY = 50; + + public static void main(String[] args) { + // Queue for incoming messages represented as Flux + // Imagine that every fireAndForget that is pushed is processed by a worker + BlockingQueue tasksQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); + + ThreadPoolExecutor threadPoolExecutor = + new ThreadPoolExecutor(1, CONCURRENT_WORKERS_COUNT, 1, TimeUnit.MINUTES, tasksQueue); + + Scheduler workScheduler = Schedulers.fromExecutorService(threadPoolExecutor); + + LeaseManager leaseManager = new LeaseManager(CONCURRENT_WORKERS_COUNT, TASK_PROCESSING_TIME); + + Disposable.Composite disposable = Disposables.composite(); + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.with( + new TasksHandlingRSocket(disposable, workScheduler, TASK_PROCESSING_TIME))) + .lease( + (config) -> + config.sender( + new LimitBasedLeaseSender( + UUID.randomUUID().toString(), + leaseManager, + VegasLimit.newBuilder() + .initialLimit(CONCURRENT_WORKERS_COUNT) + .maxConcurrency(QUEUE_CAPACITY) + .build()))) + .bindNow(QuicTransportFactory.server("localhost", 7000)); + + disposable.add(server); + + logger.info("Server started on port {}", server.address().getPort()); + server.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/simple/LeaseExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/simple/LeaseExample.java new file mode 100644 index 000000000..930afe834 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/lease/simple/LeaseExample.java @@ -0,0 +1,159 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.quic.lease.simple; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.lease.Lease; +import io.rsocket.lease.LeaseSender; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.util.ByteBufPayload; +import java.time.Duration; +import java.util.Objects; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class LeaseExample { + + private static final Logger logger = LoggerFactory.getLogger(LeaseExample.class); + + private static final String SERVER_TAG = "server"; + private static final String CLIENT_TAG = "client"; + + public static void main(String[] args) { + // Queue for incoming messages represented as Flux + // Imagine that every fireAndForget that is pushed is processed by a worker + + int queueCapacity = 50; + BlockingQueue messagesQueue = new ArrayBlockingQueue<>(queueCapacity); + + // emulating a worker that process data from the queue + Thread workerThread = + new Thread( + () -> { + try { + while (!Thread.currentThread().isInterrupted()) { + String message = messagesQueue.take(); + logger.info("Process message {}", message); + Thread.sleep(500); // emulating processing + } + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + workerThread.start(); + + CloseableChannel server = + RSocketServer.create( + (setup, sendingSocket) -> + Mono.just( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + // add element. if overflows errors and terminates execution + // specifically to show that lease can limit rate of fnf requests in + // that example + try { + if (!messagesQueue.offer(payload.getDataUtf8())) { + logger.error("Queue has been overflowed. Terminating execution"); + sendingSocket.dispose(); + workerThread.interrupt(); + } + } finally { + payload.release(); + } + return Mono.empty(); + } + })) + .lease(leases -> leases.sender(new LeaseCalculator(SERVER_TAG, messagesQueue))) + .bindNow(QuicTransportFactory.server("localhost", 7000)); + + RSocket clientRSocket = + RSocketConnector.create() + .lease((config) -> config.maxPendingRequests(1)) + .connect(QuicTransportFactory.client(server.address())) + .block(); + + Objects.requireNonNull(clientRSocket); + + // generate stream of fnfs + Flux.generate( + () -> 0L, + (state, sink) -> { + sink.next(state); + return state + 1; + }) + // here we wait for the first lease for the responder side and start execution + // on if there is allowance + .concatMap( + tick -> { + logger.info("Requesting FireAndForget({})", tick); + return clientRSocket.fireAndForget(ByteBufPayload.create("" + tick)); + }) + .blockLast(); + + clientRSocket.onClose().block(); + server.dispose(); + } + + /** + * This is a class responsible for making decision on whether Responder is ready to receive new + * FireAndForget or not base in the number of messages enqueued.
+ * In the nutshell this is responder-side rate-limiter logic which is created for every new + * connection.
+ * In real-world projects this class has to issue leases based on real metrics + */ + private static class LeaseCalculator implements LeaseSender { + final String tag; + final BlockingQueue queue; + + public LeaseCalculator(String tag, BlockingQueue queue) { + this.tag = tag; + this.queue = queue; + } + + @Override + public Flux send() { + Duration ttlDuration = Duration.ofSeconds(5); + // The interval function is used only for the demo purpose and should not be + // considered as the way to issue leases. + // For advanced RateLimiting with Leasing + // consider adopting https://github.com/Netflix/concurrency-limits#server-limiter + return Flux.interval(Duration.ZERO, ttlDuration.dividedBy(2)) + .handle( + (__, sink) -> { + // put queue.remainingCapacity() + 1 here if you want to observe that app is + // terminated because of the queue overflowing + int requests = queue.remainingCapacity(); + + // reissue new lease only if queue has remaining capacity to + // accept more requests + if (requests > 0) { + sink.next(Lease.create(ttlDuration, requests)); + } + }); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/loadbalancer/RoundRobinRSocketLoadbalancerExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/loadbalancer/RoundRobinRSocketLoadbalancerExample.java new file mode 100644 index 000000000..13e290f1d --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/loadbalancer/RoundRobinRSocketLoadbalancerExample.java @@ -0,0 +1,109 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.quic.loadbalancer; + +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketClient; +import io.rsocket.core.RSocketServer; +import io.rsocket.loadbalance.LoadbalanceRSocketClient; +import io.rsocket.loadbalance.LoadbalanceTarget; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class RoundRobinRSocketLoadbalancerExample { + + public static void main(String[] args) { + CloseableChannel server1 = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + p -> { + System.out.println("Server 1 got fnf " + p.getDataUtf8()); + return Mono.just(DefaultPayload.create("Server 1 response")) + .delayElement(Duration.ofMillis(100)); + })) + .bindNow(QuicTransportFactory.server(8080)); + + CloseableChannel server2 = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + p -> { + System.out.println("Server 2 got fnf " + p.getDataUtf8()); + return Mono.just(DefaultPayload.create("Server 2 response")) + .delayElement(Duration.ofMillis(100)); + })) + .bindNow(QuicTransportFactory.server(8081)); + + CloseableChannel server3 = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + p -> { + System.out.println("Server 3 got fnf " + p.getDataUtf8()); + return Mono.just(DefaultPayload.create("Server 3 response")) + .delayElement(Duration.ofMillis(100)); + })) + .bindNow(QuicTransportFactory.server(8082)); + + LoadbalanceTarget target8080 = LoadbalanceTarget.from("8080", QuicTransportFactory.client(8080)); + LoadbalanceTarget target8081 = LoadbalanceTarget.from("8081", QuicTransportFactory.client(8081)); + LoadbalanceTarget target8082 = LoadbalanceTarget.from("8082", QuicTransportFactory.client(8082)); + + Flux> producer = + Flux.interval(Duration.ofSeconds(5)) + .log() + .map( + i -> { + int val = i.intValue(); + switch (val) { + case 0: + return Collections.emptyList(); + case 1: + return Collections.singletonList(target8080); + case 2: + return Arrays.asList(target8080, target8081); + case 3: + return Arrays.asList(target8080, target8082); + case 4: + return Arrays.asList(target8081, target8082); + case 5: + return Arrays.asList(target8080, target8081, target8082); + case 6: + return Collections.emptyList(); + case 7: + return Collections.emptyList(); + default: + return Arrays.asList(target8080, target8081, target8082); + } + }); + + RSocketClient rSocketClient = + LoadbalanceRSocketClient.builder(producer).roundRobinLoadbalanceStrategy().build(); + + for (int i = 0; i < 10000; i++) { + try { + rSocketClient.requestResponse(Mono.just(DefaultPayload.create("test" + i))).block(); + } catch (Throwable t) { + // no ops + } + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/CompositeMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/CompositeMetadataExample.java new file mode 100644 index 000000000..f1f6cf6dd --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/CompositeMetadataExample.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015-Present the original author or 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.rsocket.examples.transport.quic.metadata.routing; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.metadata.CompositeMetadata; +import io.rsocket.metadata.CompositeMetadataCodec; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TaggingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.util.ByteBufPayload; +import java.util.Collections; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public class CompositeMetadataExample { + static final Logger logger = LoggerFactory.getLogger(CompositeMetadataExample.class); + + public static void main(String[] args) { + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); + + logger.info("Received RequestResponse[route={}]", route); + + payload.release(); + + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } + + return Mono.error(new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(QuicTransportFactory.server("localhost", 7000)); + + RSocket socket = + RSocketConnector.create() + // here we specify that every metadata payload will be encoded using + // CompositeMetadata layout as specified in the following subspec + // https://github.com/rsocket/rsocket/blob/master/Extensions/CompositeMetadata.md + .metadataMimeType(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString()) + .connect(QuicTransportFactory.client("localhost", 7000)) + .block(); + + final ByteBuf routeMetadata = + TaggingMetadataCodec.createTaggingContent( + ByteBufAllocator.DEFAULT, Collections.singletonList("my.test.route")); + final CompositeByteBuf compositeMetadata = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataCodec.encodeAndAddMetadata( + compositeMetadata, + ByteBufAllocator.DEFAULT, + WellKnownMimeType.MESSAGE_RSOCKET_ROUTING, + routeMetadata); + + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), compositeMetadata)) + .log() + .block(); + } + + static String decodeRoute(ByteBuf metadata) { + final CompositeMetadata compositeMetadata = new CompositeMetadata(metadata, false); + + for (CompositeMetadata.Entry metadatum : compositeMetadata) { + if (Objects.requireNonNull(metadatum.getMimeType()) + .equals(WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString())) { + return new RoutingMetadata(metadatum.getContent()).iterator().next(); + } + } + + return null; + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/RoutingMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/RoutingMetadataExample.java new file mode 100644 index 000000000..764433ed3 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/RoutingMetadataExample.java @@ -0,0 +1,82 @@ +/* + * Copyright 2015-Present the original author or 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.rsocket.examples.transport.quic.metadata.routing; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.metadata.RoutingMetadata; +import io.rsocket.metadata.TaggingMetadataCodec; +import io.rsocket.metadata.WellKnownMimeType; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.util.ByteBufPayload; +import java.util.Collections; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public class RoutingMetadataExample { + static final Logger logger = LoggerFactory.getLogger(RoutingMetadataExample.class); + + public static void main(String[] args) { + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); + + logger.info("Received RequestResponse[route={}]", route); + + payload.release(); + + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } + + return Mono.error(new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(QuicTransportFactory.server("localhost", 7000)); + + RSocket socket = + RSocketConnector.create() + // here we specify that route will be encoded using + // Routing&Tagging Metadata layout specified at this + // subspec https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md + .metadataMimeType(WellKnownMimeType.MESSAGE_RSOCKET_ROUTING.getString()) + .connect(QuicTransportFactory.client("localhost", 7000)) + .block(); + + final ByteBuf routeMetadata = + TaggingMetadataCodec.createTaggingContent( + ByteBufAllocator.DEFAULT, Collections.singletonList("my.test.route")); + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), routeMetadata)) + .log() + .block(); + } + + static String decodeRoute(ByteBuf metadata) { + final RoutingMetadata routingMetadata = new RoutingMetadata(metadata); + + return routingMetadata.iterator().next(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/plugins/LimitRateInterceptorExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/plugins/LimitRateInterceptorExample.java new file mode 100644 index 000000000..9e53d80cb --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/plugins/LimitRateInterceptorExample.java @@ -0,0 +1,82 @@ +package io.rsocket.examples.transport.quic.plugins; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.plugins.LimitRateInterceptor; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +public class LimitRateInterceptorExample { + + private static final Logger logger = LoggerFactory.getLogger(LimitRateInterceptorExample.class); + + public static void main(String[] args) { + RSocketServer.create( + SocketAcceptor.with( + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + return Flux.interval(Duration.ofMillis(100)) + .doOnRequest( + e -> logger.debug("Server publisher receives request for " + e)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .doOnRequest( + e -> logger.debug("Server publisher receives request for " + e)); + } + })) + .interceptors(registry -> registry.forResponder(LimitRateInterceptor.forResponder(64))) + .bindNow(QuicTransportFactory.server("localhost", 7000)); + + RSocket socket = + RSocketConnector.create() + .interceptors(registry -> registry.forRequester(LimitRateInterceptor.forRequester(64))) + .connect(QuicTransportFactory.client("localhost", 7000)) + .block(); + + logger.debug( + "\n\nStart of requestStream interaction\n" + "----------------------------------\n"); + + socket + .requestStream(DefaultPayload.create("Hello")) + .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); + + logger.debug( + "\n\nStart of requestChannel interaction\n" + "-----------------------------------\n"); + + socket + .requestChannel( + Flux.generate( + () -> 1L, + (s, sink) -> { + sink.next(DefaultPayload.create("Next " + s)); + return ++s; + }) + .doOnRequest(e -> logger.debug("Client publisher receives request for " + e))) + .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .doFinally(signalType -> socket.dispose()) + .then() + .block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/requestresponse/HelloWorldClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/requestresponse/HelloWorldClient.java new file mode 100644 index 000000000..ffc2e7a64 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/requestresponse/HelloWorldClient.java @@ -0,0 +1,73 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.quic.requestresponse; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.util.DefaultPayload; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public final class HelloWorldClient { + + private static final Logger logger = LoggerFactory.getLogger(HelloWorldClient.class); + + public static void main(String[] args) { + + RSocket rsocket = + new RSocket() { + boolean fail = true; + + @Override + public Mono requestResponse(Payload p) { + if (fail) { + fail = false; + return Mono.error(new Throwable("Simulated error")); + } else { + return Mono.just(p); + } + } + }; + + CloseableChannel server = + RSocketServer.create(SocketAcceptor.with(rsocket)) + .bindNow(QuicTransportFactory.server("127.0.0.1", 0)); + + RSocket socket = + RSocketConnector.connectWith( + QuicTransportFactory.client("127.0.0.1", server.address().getPort())) + .block(); + + for (int i = 0; i < 3; i++) { + socket + .requestResponse(DefaultPayload.create("Hello")) + .map(Payload::getDataUtf8) + .onErrorReturn("error") + .doOnNext(logger::debug) + .block(); + } + + socket.dispose(); + server.dispose(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/resume/ResumeFileTransfer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/resume/ResumeFileTransfer.java new file mode 100644 index 000000000..1ffbe519f --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/resume/ResumeFileTransfer.java @@ -0,0 +1,119 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.quic.resume; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.core.Resume; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.examples.transport.support.DisconnectableClientTransport; +import io.rsocket.examples.transport.support.ResumeExampleSupport; +import io.rsocket.examples.transport.support.ResumeFiles; +import io.rsocket.examples.transport.support.ResumeRequest; +import io.rsocket.examples.transport.support.ResumeRequestCodec; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.IntConsumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class ResumeFileTransfer { + private static final String HOST = "127.0.0.1"; + private static final int CHUNK_SIZE = 16; + private static final String INPUT_FILE = "lorem.txt"; + private static final String OUTPUT_FILE = "build/lorem_output_quic.txt"; + + /*amount of file chunks requested by subscriber: n, refilled on n/2 of received items*/ + private static final int PREFETCH_WINDOW_SIZE = 4; + private static final Duration FILE_CHUNK_DELAY = Duration.ofMillis(50); + private static final Duration DISCONNECT_COOLDOWN = Duration.ofMillis(500); + private static final Duration RESUME_RETRY_DELAY = Duration.ofMillis(250); + private static final Set DISCONNECT_CHUNKS = Set.of(24, 96); + private static final Logger logger = LoggerFactory.getLogger(ResumeFileTransfer.class); + + public static void main(String[] args) { + AtomicBoolean shuttingDown = new AtomicBoolean(); + + try (ResumeExampleSupport.HookHandle hookHandle = + ResumeExampleSupport.suppressExpectedShutdownErrors(shuttingDown, logger)) { + hookHandle.keep(); + Resume resume = ResumeExampleSupport.resume(RESUME_RETRY_DELAY, shuttingDown, logger); + + ResumeRequestCodec codec = new ResumeRequestCodec(); + + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> { + ResumeRequest request = codec.decode(payload); + payload.release(); + + Flux ticks = Flux.interval(FILE_CHUNK_DELAY).onBackpressureDrop(); + + return ResumeFiles.fileSource(request.getFileName(), request.getChunkSize()) + .map(DefaultPayload::create) + .zipWith(ticks, (p, tick) -> p) + .log("server"); + })) + .resume(resume) + .bindNow(QuicTransportFactory.server(HOST, 0)); + + DisconnectableClientTransport clientTransport = + new DisconnectableClientTransport( + QuicTransportFactory.client(HOST, server.address().getPort())); + + RSocket client = + RSocketConnector.create() + .resume(resume) + .connect(clientTransport) + .block(); + + Mono.delay(Duration.ofMillis(250)).block(); + IntConsumer disconnectTrigger = + ResumeExampleSupport.chunkDisconnectTrigger( + DISCONNECT_CHUNKS, + clientTransport, + DISCONNECT_COOLDOWN, + shuttingDown, + logger); + + try { + ResumeExampleSupport.runRequestStream( + client, + codec.encode(new ResumeRequest(CHUNK_SIZE, INPUT_FILE)), + ResumeFiles.fileSink( + OUTPUT_FILE, + PREFETCH_WINDOW_SIZE, + disconnectTrigger, + () -> {}, + __ -> {})) + .block(); + } finally { + ResumeExampleSupport.shutdown(shuttingDown, client, clientTransport, server, logger); + } + } + } + +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/resume/readme.md b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/resume/readme.md new file mode 100644 index 000000000..aad4de80a --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/resume/readme.md @@ -0,0 +1,22 @@ +1. Start `ResumeFileTransfer.main` + +2. The example simulates two short transport disconnects in-process and resumes the stream each time + +`ResumeFileTransfer` output is as follows + +``` +Received file chunk: 7. Total size: 112 +Received file chunk: 8. Total size: 128 +Received file chunk: 9. Total size: 144 +Received file chunk: 10. Total size: 160 +Disconnected. Trying to resume... +Disconnected. Trying to resume... +Received file chunk: 11. Total size: 176 +Received file chunk: 12. Total size: 192 +Received file chunk: 13. Total size: 208 +Received file chunk: 14. Total size: 224 +Received file chunk: 15. Total size: 240 +Received file chunk: 16. Total size: 256 +``` + +It transfers file from `resources/lorem.txt` to `build/lorem_output_quic.txt` in chunks of 16 bytes every 50 millis diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ClientStreamingToServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ClientStreamingToServer.java new file mode 100644 index 000000000..d2f426867 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ClientStreamingToServer.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.quic.stream; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; + +public final class ClientStreamingToServer { + + private static final Logger logger = LoggerFactory.getLogger(ClientStreamingToServer.class); + + public static void main(String[] args) throws InterruptedException { + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> + Flux.interval(Duration.ofMillis(100)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)))) + .bindNow(QuicTransportFactory.server("localhost", 7000)); + + RSocket socket = + RSocketConnector.create() + .setupPayload(DefaultPayload.create("test", "test")) + .connect(QuicTransportFactory.client("localhost", 7000)) + .block(); + + final Payload payload = DefaultPayload.create("Hello"); + socket + .requestStream(payload) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .doFinally(signalType -> socket.dispose()) + .then() + .block(); + + Thread.sleep(1000000); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ServerStreamingToClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ServerStreamingToClient.java new file mode 100644 index 000000000..f5796de37 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ServerStreamingToClient.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015-2020 the original author or 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.rsocket.examples.transport.quic.stream; + +import static io.rsocket.SocketAcceptor.forRequestStream; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.util.DefaultPayload; +import java.time.Duration; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public final class ServerStreamingToClient { + + public static void main(String[] args) { + + RSocketServer.create( + (setup, rsocket) -> { + rsocket + .requestStream(DefaultPayload.create("Hello-Bidi")) + .map(Payload::getDataUtf8) + .log() + .subscribe(); + + return Mono.just(new RSocket() {}); + }) + .bindNow(QuicTransportFactory.server("localhost", 7000)); + + RSocket rsocket = + RSocketConnector.create() + .acceptor( + forRequestStream( + payload -> + Flux.interval(Duration.ofSeconds(1)) + .map(aLong -> DefaultPayload.create("Bi-di Response => " + aLong)))) + .connect(QuicTransportFactory.client("localhost", 7000)) + .block(); + + rsocket.onClose().block(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/DisconnectableClientTransport.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/DisconnectableClientTransport.java new file mode 100644 index 000000000..447cb8043 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/DisconnectableClientTransport.java @@ -0,0 +1,83 @@ +/* + * Copyright 2015-2026 the original author or 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.rsocket.examples.transport.support; + +import io.rsocket.DuplexConnection; +import io.rsocket.transport.ClientTransport; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicReference; +import reactor.core.publisher.Mono; + +public final class DisconnectableClientTransport implements ClientTransport { + private final ClientTransport clientTransport; + private final AtomicReference currentConnection = new AtomicReference<>(); + private volatile long nextConnectPermitMillis; + + public DisconnectableClientTransport(ClientTransport clientTransport) { + this.clientTransport = clientTransport; + } + + @Override + public Mono connect() { + return Mono.defer( + () -> + now() < nextConnectPermitMillis + ? Mono.error(new ClosedChannelException()) + : clientTransport + .connect() + .map( + connection -> { + if (currentConnection.compareAndSet(null, connection)) { + return connection; + } + throw new IllegalStateException( + "Transport supports at most 1 connection"); + })); + } + + public void disconnect() { + disconnectFor(Duration.ZERO); + } + + public void dispose() { + nextConnectPermitMillis = Long.MAX_VALUE; + DuplexConnection current = currentConnection.getAndSet(null); + if (current != null && !current.isDisposed()) { + current.dispose(); + } + } + + public void disconnectFor(Duration cooldown) { + DuplexConnection current = currentConnection.getAndSet(null); + if (current == null) { + throw new IllegalStateException("Trying to disconnect while not connected"); + } + + nextConnectPermitMillis = now() + cooldown.toMillis(); + current.dispose(); + } + + public boolean isConnected() { + DuplexConnection current = currentConnection.get(); + return current != null && !current.isDisposed(); + } + + private static long now() { + return System.currentTimeMillis(); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeExampleSupport.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeExampleSupport.java new file mode 100644 index 000000000..55857b923 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeExampleSupport.java @@ -0,0 +1,208 @@ +/* + * Copyright 2015-2026 the original author or 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.rsocket.examples.transport.support; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.Resume; +import io.rsocket.transport.netty.server.CloseableChannel; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntConsumer; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.util.retry.Retry; + +public final class ResumeExampleSupport { + private static final Duration CLOSE_TIMEOUT = Duration.ofSeconds(1); + + private ResumeExampleSupport() {} + + public static HookHandle suppressExpectedShutdownErrors( + AtomicBoolean shuttingDown, Logger logger) { + Hooks.onErrorDropped( + t -> { + if (!(shuttingDown.get() && causedByClosedChannel(t))) { + logger.error("Unexpected dropped error", t); + } + }); + return new HookHandle(); + } + + private static boolean causedByClosedChannel(Throwable throwable) { + Throwable current = throwable; + while (current != null) { + if (current instanceof ClosedChannelException) { + return true; + } + current = current.getCause(); + } + return false; + } + + public static Resume resume( + Duration retryDelay, AtomicBoolean shuttingDown, Logger logger) { + return new Resume() + .sessionDuration(Duration.ofMinutes(5)) + .retry( + Retry.fixedDelay(Long.MAX_VALUE, retryDelay) + .doBeforeRetry( + s -> { + if (!shuttingDown.get()) { + logger.debug("Disconnected. Trying to resume..."); + } + })); + } + + public static Mono runRequestStream( + RSocket client, Payload request, Subscriber subscriber) { + return Mono.create( + sink -> + client + .requestStream(request) + .log("client") + .subscribe( + new Subscriber() { + @Override + public void onSubscribe(Subscription s) { + subscriber.onSubscribe(s); + } + + @Override + public void onNext(Payload payload) { + subscriber.onNext(payload); + } + + @Override + public void onError(Throwable t) { + try { + subscriber.onError(t); + } finally { + sink.error(t); + } + } + + @Override + public void onComplete() { + try { + subscriber.onComplete(); + } finally { + sink.success(); + } + } + })); + } + + public static IntConsumer chunkDisconnectTrigger( + Set disconnectChunks, + DisconnectableClientTransport clientTransport, + Duration disconnectCooldown, + AtomicBoolean shuttingDown, + Logger logger) { + AtomicInteger disconnectAttempts = new AtomicInteger(); + return receivedChunk -> { + if (!shuttingDown.get() + && disconnectChunks.contains(receivedChunk) + && clientTransport.isConnected()) { + int attempt = disconnectAttempts.incrementAndGet(); + logger.debug("Simulating transport disconnect #{}", attempt); + clientTransport.disconnectFor(disconnectCooldown); + } + }; + } + + public static Disposable intervalDisconnectSimulation( + Duration disconnectInterval, + long attempts, + DisconnectableClientTransport clientTransport, + Duration disconnectCooldown, + AtomicBoolean shuttingDown, + Logger logger) { + return Flux.interval(disconnectInterval) + .take(attempts) + .subscribe( + attempt -> { + if (!shuttingDown.get() && clientTransport.isConnected()) { + logger.debug("Simulating transport disconnect #{}", attempt + 1); + clientTransport.disconnectFor(disconnectCooldown); + } + }); + } + + public static void shutdown( + AtomicBoolean shuttingDown, + RSocket client, + DisconnectableClientTransport clientTransport, + CloseableChannel server, + Logger logger) { + shuttingDown.set(true); + + try { + clientTransport.dispose(); + } catch (Throwable t) { + logger.debug("Failed to dispose client transport cleanly", t); + } + + shutdown(client, server, logger); + } + + public static void shutdown(RSocket client, CloseableChannel server, Logger logger) { + try { + client.dispose(); + } catch (Throwable t) { + logger.debug("Failed to dispose client cleanly", t); + } + + try { + client.onClose().block(CLOSE_TIMEOUT); + } catch (Throwable t) { + logger.debug("Client close timed out", t); + } + + try { + server.dispose(); + } catch (Throwable t) { + logger.debug("Failed to dispose server cleanly", t); + } + + try { + server.onClose().block(CLOSE_TIMEOUT); + } catch (Throwable t) { + logger.debug("Server close timed out", t); + } + + Schedulers.shutdownNow(); + } + + public static final class HookHandle implements AutoCloseable { + public void keep() {} + + @Override + public void close() { + Hooks.resetOnErrorDropped(); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/Files.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeFiles.java similarity index 62% rename from rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/Files.java rename to rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeFiles.java index 6724ca93f..5be7ae402 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/Files.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeFiles.java @@ -1,14 +1,33 @@ -package io.rsocket.examples.transport.tcp.resume; +/* + * Copyright 2015-2026 the original author or 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.rsocket.examples.transport.support; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.Payload; import java.io.BufferedInputStream; +import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.function.Consumer; +import java.util.function.IntConsumer; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import org.slf4j.Logger; @@ -16,8 +35,10 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.SynchronousSink; -class Files { - private static final Logger logger = LoggerFactory.getLogger(Files.class); +public final class ResumeFiles { + private static final Logger logger = LoggerFactory.getLogger(ResumeFiles.class); + + private ResumeFiles() {} public static Flux fileSource(String fileName, int chunkSizeBytes) { return Flux.generate( @@ -25,6 +46,20 @@ public static Flux fileSource(String fileName, int chunkSizeBytes) { } public static Subscriber fileSink(String fileName, int windowSize) { + return fileSink(fileName, windowSize, __ -> {}, () -> {}, __ -> {}); + } + + public static Subscriber fileSink( + String fileName, int windowSize, Runnable onComplete, Consumer onError) { + return fileSink(fileName, windowSize, __ -> {}, onComplete, onError); + } + + public static Subscriber fileSink( + String fileName, + int windowSize, + IntConsumer onChunkReceived, + Runnable onComplete, + Consumer onError) { return new Subscriber() { Subscription s; int requests = windowSize; @@ -49,6 +84,7 @@ public void onNext(Payload payload) { } write(outputStream, data); payload.release(); + onChunkReceived.accept(receivedCount); requests--; if (requests == windowSize / 2) { @@ -68,19 +104,27 @@ private void write(OutputStream outputStream, ByteBuf byteBuf) { @Override public void onError(Throwable t) { close(outputStream); + onError.accept(t); } @Override public void onComplete() { close(outputStream); + onComplete.run(); } private OutputStream open(String filename) { try { - /*do not buffer for demo purposes*/ - return new FileOutputStream(filename); + File file = new File(filename); + File parent = file.getParentFile(); + if (parent != null && !parent.exists() && !parent.mkdirs() && !parent.exists()) { + throw new IOException("Could not create directory " + parent); + } + return new FileOutputStream(file); } catch (FileNotFoundException e) { throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); } } @@ -101,20 +145,20 @@ private static class FileState { private BufferedInputStream inputStream; private byte[] chunkBytes; - public FileState(String fileName, int chunkSizeBytes) { + FileState(String fileName, int chunkSizeBytes) { this.fileName = fileName; this.chunkSizeBytes = chunkSizeBytes; } - public FileState consumeNext(SynchronousSink sink) { + FileState consumeNext(SynchronousSink sink) { if (inputStream == null) { InputStream in = getClass().getClassLoader().getResourceAsStream(fileName); if (in == null) { sink.error(new FileNotFoundException(fileName)); return this; } - this.inputStream = new BufferedInputStream(in); - this.chunkBytes = new byte[chunkSizeBytes]; + inputStream = new BufferedInputStream(in); + chunkBytes = new byte[chunkSizeBytes]; } try { int consumedBytes = inputStream.read(chunkBytes); @@ -129,7 +173,7 @@ public FileState consumeNext(SynchronousSink sink) { return this; } - public void dispose() { + void dispose() { if (inputStream != null) { try { inputStream.close(); diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeRequest.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeRequest.java new file mode 100644 index 000000000..f9abecf6c --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeRequest.java @@ -0,0 +1,35 @@ +/* + * Copyright 2015-2026 the original author or 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.rsocket.examples.transport.support; + +public final class ResumeRequest { + private final int chunkSize; + private final String fileName; + + public ResumeRequest(int chunkSize, String fileName) { + this.chunkSize = chunkSize; + this.fileName = fileName; + } + + public int getChunkSize() { + return chunkSize; + } + + public String getFileName() { + return fileName; + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeRequestCodec.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeRequestCodec.java new file mode 100644 index 000000000..f0f411806 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ResumeRequestCodec.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2026 the original author or 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.rsocket.examples.transport.support; + +import io.rsocket.Payload; +import io.rsocket.util.DefaultPayload; + +public final class ResumeRequestCodec { + + public Payload encode(ResumeRequest request) { + String encoded = request.getChunkSize() + ":" + request.getFileName(); + return DefaultPayload.create(encoded); + } + + public ResumeRequest decode(Payload payload) { + String encoded = payload.getDataUtf8(); + String[] chunkSizeAndFileName = encoded.split(":"); + int chunkSize = Integer.parseInt(chunkSizeAndFileName[0]); + String fileName = chunkSizeAndFileName[1]; + return new ResumeRequest(chunkSize, fileName); + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java index ba82c7c93..6e841f63e 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java @@ -22,6 +22,10 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.core.Resume; +import io.rsocket.examples.transport.support.ResumeExampleSupport; +import io.rsocket.examples.transport.support.ResumeFiles; +import io.rsocket.examples.transport.support.ResumeRequest; +import io.rsocket.examples.transport.support.ResumeRequestCodec; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; @@ -33,9 +37,14 @@ import reactor.util.retry.Retry; public class ResumeFileTransfer { + private static final String HOST = "localhost"; + private static final int CHUNK_SIZE = 16; + private static final String INPUT_FILE = "lorem.txt"; + private static final String OUTPUT_FILE = "build/lorem_output_tcp.txt"; /*amount of file chunks requested by subscriber: n, refilled on n/2 of received items*/ private static final int PREFETCH_WINDOW_SIZE = 4; + private static final Duration FILE_CHUNK_DELAY = Duration.ofMillis(50); private static final Logger logger = LoggerFactory.getLogger(ResumeFileTransfer.class); public static void main(String[] args) { @@ -47,73 +56,39 @@ public static void main(String[] args) { Retry.fixedDelay(Long.MAX_VALUE, Duration.ofSeconds(1)) .doBeforeRetry(s -> logger.debug("Disconnected. Trying to resume..."))); - RequestCodec codec = new RequestCodec(); + ResumeRequestCodec codec = new ResumeRequestCodec(); CloseableChannel server = RSocketServer.create( SocketAcceptor.forRequestStream( payload -> { - Request request = codec.decode(payload); + ResumeRequest request = codec.decode(payload); payload.release(); - String fileName = request.getFileName(); - int chunkSize = request.getChunkSize(); - Flux ticks = Flux.interval(Duration.ofMillis(500)).onBackpressureDrop(); + Flux ticks = Flux.interval(FILE_CHUNK_DELAY).onBackpressureDrop(); - return Files.fileSource(fileName, chunkSize) + return ResumeFiles.fileSource(request.getFileName(), request.getChunkSize()) .map(DefaultPayload::create) .zipWith(ticks, (p, tick) -> p) .log("server"); })) .resume(resume) - .bindNow(TcpServerTransport.create("localhost", 8000)); + .bindNow(TcpServerTransport.create(HOST, 0)); RSocket client = RSocketConnector.create() .resume(resume) - .connect(TcpClientTransport.create("localhost", 8001)) + .connect(TcpClientTransport.create(HOST, server.address().getPort())) .block(); - client - .requestStream(codec.encode(new Request(16, "lorem.txt"))) - .log("client") - .doFinally(s -> server.dispose()) - .subscribe(Files.fileSink("rsocket-examples/build/lorem_output.txt", PREFETCH_WINDOW_SIZE)); - - server.onClose().block(); - } - - private static class RequestCodec { - - public Payload encode(Request request) { - String encoded = request.getChunkSize() + ":" + request.getFileName(); - return DefaultPayload.create(encoded); - } - - public Request decode(Payload payload) { - String encoded = payload.getDataUtf8(); - String[] chunkSizeAndFileName = encoded.split(":"); - int chunkSize = Integer.parseInt(chunkSizeAndFileName[0]); - String fileName = chunkSizeAndFileName[1]; - return new Request(chunkSize, fileName); - } - } - - private static class Request { - private final int chunkSize; - private final String fileName; - - public Request(int chunkSize, String fileName) { - this.chunkSize = chunkSize; - this.fileName = fileName; - } - - public int getChunkSize() { - return chunkSize; - } - - public String getFileName() { - return fileName; + try { + ResumeExampleSupport.runRequestStream( + client, + codec.encode(new ResumeRequest(CHUNK_SIZE, INPUT_FILE)), + ResumeFiles.fileSink(OUTPUT_FILE, PREFETCH_WINDOW_SIZE, () -> {}, __ -> {})) + .block(); + } finally { + ResumeExampleSupport.shutdown(client, server, logger); } } } diff --git a/scripts/run-example.sh b/scripts/run-example.sh new file mode 100755 index 000000000..9cd120bd8 --- /dev/null +++ b/scripts/run-example.sh @@ -0,0 +1,213 @@ +#!/usr/bin/env bash + +set -euo pipefail +shopt -s nullglob + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +CACHE_DIR="${HOME}/.gradle/caches/modules-2/files-2.1" + +usage() { + cat <<'EOF' +Usage: scripts/run-example.sh [args...] + +Runs an rsocket-java example directly with `java`, without using Gradle at runtime. + +Prerequisite: + Build the example classes and module jars once first, for example: + + export JAVA_HOME=$(/usr/libexec/java_home -v 13) + export PATH="$JAVA_HOME/bin:$PATH" + ./gradlew :rsocket-examples:classes \ + :rsocket-examples:jar \ + :rsocket-core:jar \ + :rsocket-load-balancer:jar \ + :rsocket-micrometer:jar \ + :rsocket-transport-netty:jar \ + :rsocket-transport-quic:jar \ + :rsocket-transport-h3:jar \ + :rsocket-transport-local:jar + +Example: + scripts/run-example.sh io.rsocket.examples.transport.h3.resume.ResumeFileTransfer +EOF +} + +if [[ $# -lt 1 ]]; then + usage + exit 1 +fi + +MAIN_CLASS="$1" +shift + +if [[ -z "${JAVA_HOME:-}" ]] && command -v /usr/libexec/java_home >/dev/null 2>&1; then + export JAVA_HOME="$("/usr/libexec/java_home" -v 13)" +fi + +if [[ -n "${JAVA_HOME:-}" ]]; then + export PATH="${JAVA_HOME}/bin:${PATH}" +fi + +cd "$ROOT_DIR" + +require_path() { + local path="$1" + local help_text="$2" + if [[ ! -e "$path" ]]; then + echo "Missing required path: $path" >&2 + echo "$help_text" >&2 + exit 1 + fi +} + +find_single_jar() { + local description="$1" + shift + local matches=("$@") + local runtime_matches=() + local jar + for jar in "${matches[@]}"; do + if [[ "$jar" != *-sources.jar && "$jar" != *-javadoc.jar ]]; then + runtime_matches+=("$jar") + fi + done + if [[ ${#runtime_matches[@]} -gt 0 ]]; then + matches=("${runtime_matches[@]}") + fi + if [[ ${#matches[@]} -eq 0 ]]; then + echo "Missing dependency jar for ${description}" >&2 + echo "Try building once with ./gradlew and make sure Gradle dependencies are cached." >&2 + exit 1 + fi + printf '%s\n' "${matches[0]}" +} + +os_name="$(uname -s)" +os_arch="$(uname -m)" +quic_classifier="" +case "${os_name}:${os_arch}" in + Darwin:arm64) quic_classifier="osx-aarch_64" ;; + Darwin:x86_64) quic_classifier="osx-x86_64" ;; + Linux:x86_64) quic_classifier="linux-x86_64" ;; + Linux:aarch64) quic_classifier="linux-aarch_64" ;; + MINGW*:x86_64|MSYS*:x86_64|CYGWIN*:x86_64) quic_classifier="windows-x86_64" ;; +esac + +require_path "rsocket-examples/build/classes/java/main" \ + "Run the Gradle build step shown in the script usage first." + +classpath_entries=( + "rsocket-examples/build/classes/java/main" + "rsocket-examples/build/resources/main" + "rsocket-core/build/classes/java/main" + "rsocket-load-balancer/build/classes/java/main" + "rsocket-micrometer/build/classes/java/main" + "rsocket-transport-netty/build/classes/java/main" + "rsocket-transport-local/build/classes/java/main" + "rsocket-transport-quic/build/classes/java/main" + "rsocket-transport-h3/build/classes/java/main" +) + +optional_project_resources=( + "rsocket-core/build/resources/main" + "rsocket-load-balancer/build/resources/main" + "rsocket-micrometer/build/resources/main" + "rsocket-transport-netty/build/resources/main" + "rsocket-transport-local/build/resources/main" + "rsocket-transport-quic/build/resources/main" + "rsocket-transport-h3/build/resources/main" +) + +for path in "${optional_project_resources[@]}"; do + if [[ -e "$path" ]]; then + classpath_entries+=("$path") + fi +done + +dependency_roots=( + "aopalliance" + "ch.qos.logback" + "com.netflix.concurrency-limits" + "io.micrometer" + "io.netty" + "io.netty.incubator" + "io.projectreactor" + "io.projectreactor.netty" + "org.hdrhistogram" + "org.latencyutils" + "org.reactivestreams" + "org.slf4j" +) + +add_latest_cached_jars() { + local root="$1" + local artifact_dir + while IFS= read -r artifact_dir; do + local artifact_name + local preferred_version + local latest_version + artifact_name="$(basename "$artifact_dir")" + if [[ "$artifact_name" == "slf4j-simple" ]]; then + continue + fi + preferred_version="" + case "$artifact_name" in + logback-classic|logback-core) preferred_version="1.2.13" ;; + slf4j-api) preferred_version="1.7.36" ;; + esac + if [[ -n "$preferred_version" && -d "$artifact_dir/$preferred_version" ]]; then + latest_version="$preferred_version" + else + latest_version="$( + find "$artifact_dir" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort -V | tail -n 1 + )" + fi + if [[ -z "$latest_version" ]]; then + continue + fi + while IFS= read -r jar; do + classpath_entries+=("$jar") + done < <(find "$artifact_dir/$latest_version" -type f -name '*.jar' | sort) + done < <(find "$root" -mindepth 1 -maxdepth 1 -type d | sort) +} + +for root in "${dependency_roots[@]}"; do + if [[ -d "${CACHE_DIR}/${root}" ]]; then + add_latest_cached_jars "${CACHE_DIR}/${root}" + fi +done + +if [[ -n "$quic_classifier" ]]; then + native_quic_root="${CACHE_DIR}/io.netty.incubator/netty-incubator-codec-native-quic" + if [[ -d "$native_quic_root" ]]; then + latest_native_quic_version="$( + find "$native_quic_root" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort -V | tail -n 1 + )" + native_quic_jars=("${native_quic_root}/${latest_native_quic_version}"/*/*"${quic_classifier}.jar") + if [[ ${#native_quic_jars[@]} -gt 0 ]]; then + classpath_entries+=("${native_quic_jars[@]}") + fi + fi +fi + +tmp_args="/tmp/rsocket-example-cp-$$-$(date +%s).args" +touch "$tmp_args" +cleanup() { + rm -f "$tmp_args" +} +trap cleanup EXIT + +{ + echo "-cp" + ( + IFS=: + printf '%s' "${classpath_entries[*]}" + ) + echo + echo "$MAIN_CLASS" + for arg in "$@"; do + echo "$arg" + done +} >"$tmp_args" + +exec java @"$tmp_args" From 04a0488f15d895421c750577610feb22ac572a11 Mon Sep 17 00:00:00 2001 From: "jeroen.veltman" Date: Thu, 2 Apr 2026 21:28:32 +0200 Subject: [PATCH 08/11] Add example discovery to run-example script Signed-off-by: jeroen.veltman --- scripts/run-example.sh | 69 ++++++++++++++++++++++++++++++++++++------ 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/scripts/run-example.sh b/scripts/run-example.sh index 9cd120bd8..c13cd28c6 100755 --- a/scripts/run-example.sh +++ b/scripts/run-example.sh @@ -8,7 +8,10 @@ CACHE_DIR="${HOME}/.gradle/caches/modules-2/files-2.1" usage() { cat <<'EOF' -Usage: scripts/run-example.sh [args...] +Usage: + scripts/run-example.sh [args...] + scripts/run-example.sh --list + scripts/run-example.sh --list-full Runs an rsocket-java example directly with `java`, without using Gradle at runtime. @@ -29,17 +32,12 @@ Prerequisite: Example: scripts/run-example.sh io.rsocket.examples.transport.h3.resume.ResumeFileTransfer + +List all runnable examples: + scripts/run-example.sh --list EOF } -if [[ $# -lt 1 ]]; then - usage - exit 1 -fi - -MAIN_CLASS="$1" -shift - if [[ -z "${JAVA_HOME:-}" ]] && command -v /usr/libexec/java_home >/dev/null 2>&1; then export JAVA_HOME="$("/usr/libexec/java_home" -v 13)" fi @@ -50,6 +48,59 @@ fi cd "$ROOT_DIR" +list_examples() { + rg -l 'public\s+static\s+void\s+main\s*\(' rsocket-examples/src/main/java/io/rsocket/examples \ + | sort \ + | sed 's#rsocket-examples/src/main/java/##' \ + | sed 's#/#.#g' \ + | sed 's#\.java$##' +} + +print_grouped_examples() { + local current_group="" + local example + + while IFS= read -r example; do + local group + group="$(printf '%s\n' "$example" | sed 's#^io\.rsocket\.examples\.transport\.##' | sed 's#\.[^.]*$##' | tr '.' '/')" + if [[ "$group" == "$example" ]]; then + group="other" + fi + + if [[ "$group" != "$current_group" ]]; then + if [[ -n "$current_group" ]]; then + echo + fi + printf '%s\n' "$group" + current_group="$group" + fi + printf ' %s\n' "$example" + done < <(list_examples) +} + +if [[ $# -lt 1 ]]; then + usage + exit 1 +fi + +case "${1:-}" in + --help|-h) + usage + exit 0 + ;; + --list) + print_grouped_examples + exit 0 + ;; + --list-full) + list_examples + exit 0 + ;; +esac + +MAIN_CLASS="$1" +shift + require_path() { local path="$1" local help_text="$2" From fa9537331080918090c7981f6fa105042979bb68 Mon Sep 17 00:00:00 2001 From: "jeroen.veltman" Date: Thu, 2 Apr 2026 21:31:15 +0200 Subject: [PATCH 09/11] Add grep fallback for example discovery Signed-off-by: jeroen.veltman --- scripts/run-example.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/run-example.sh b/scripts/run-example.sh index c13cd28c6..150124af1 100755 --- a/scripts/run-example.sh +++ b/scripts/run-example.sh @@ -49,7 +49,12 @@ fi cd "$ROOT_DIR" list_examples() { - rg -l 'public\s+static\s+void\s+main\s*\(' rsocket-examples/src/main/java/io/rsocket/examples \ + if command -v rg >/dev/null 2>&1; then + rg -l 'public\s+static\s+void\s+main\s*\(' rsocket-examples/src/main/java/io/rsocket/examples + else + find rsocket-examples/src/main/java/io/rsocket/examples -type f -name '*.java' -print0 \ + | xargs -0 grep -l 'public[[:space:]]\+static[[:space:]]\+void[[:space:]]\+main[[:space:]]*(' + fi \ | sort \ | sed 's#rsocket-examples/src/main/java/##' \ | sed 's#/#.#g' \ From c49ebbf245f8712eeed5b522378de61ddc2ffce5 Mon Sep 17 00:00:00 2001 From: "jeroen.veltman" Date: Thu, 2 Apr 2026 21:54:46 +0200 Subject: [PATCH 10/11] Clean up example shutdown behavior Signed-off-by: jeroen.veltman --- .../h3/channel/ChannelEchoClient.java | 29 +++-- ...ingWithServerSideNotificationsExample.java | 40 ++++--- .../RoundRobinRSocketLoadbalancerExample.java | 18 +++- .../routing/CompositeMetadataExample.java | 46 ++++---- .../routing/RoutingMetadataExample.java | 45 ++++---- .../plugins/LimitRateInterceptorExample.java | 100 ++++++++++-------- .../h3/requestresponse/HelloWorldClient.java | 28 +++-- .../h3/stream/ClientStreamingToServer.java | 40 +++---- .../h3/stream/ServerStreamingToClient.java | 38 +++++-- .../quic/channel/ChannelEchoClient.java | 29 +++-- ...ingWithServerSideNotificationsExample.java | 40 ++++--- .../RoundRobinRSocketLoadbalancerExample.java | 18 +++- .../routing/CompositeMetadataExample.java | 46 ++++---- .../routing/RoutingMetadataExample.java | 45 ++++---- .../plugins/LimitRateInterceptorExample.java | 100 ++++++++++-------- .../requestresponse/HelloWorldClient.java | 23 ++-- .../quic/stream/ClientStreamingToServer.java | 40 +++---- .../quic/stream/ServerStreamingToClient.java | 38 +++++-- .../transport/support/ExampleLifecycle.java | 59 +++++++++++ .../tcp/channel/ChannelEchoClient.java | 29 +++-- ...ingWithServerSideNotificationsExample.java | 40 ++++--- .../RoundRobinRSocketLoadbalancerExample.java | 18 +++- .../routing/CompositeMetadataExample.java | 46 ++++---- .../routing/RoutingMetadataExample.java | 45 ++++---- .../plugins/LimitRateInterceptorExample.java | 98 +++++++++-------- .../tcp/requestresponse/HelloWorldClient.java | 28 +++-- .../tcp/stream/ClientStreamingToServer.java | 40 +++---- .../tcp/stream/ServerStreamingToClient.java | 38 +++++-- .../ws/WebSocketAggregationSample.java | 16 +-- .../transport/ws/WebSocketHeadersSample.java | 34 +++--- 30 files changed, 782 insertions(+), 472 deletions(-) create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ExampleLifecycle.java diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/channel/ChannelEchoClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/channel/ChannelEchoClient.java index f5f4426fd..b91554053 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/channel/ChannelEchoClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/channel/ChannelEchoClient.java @@ -22,6 +22,7 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; import org.slf4j.Logger; @@ -31,6 +32,7 @@ public final class ChannelEchoClient { private static final Logger logger = LoggerFactory.getLogger(ChannelEchoClient.class); + private static final Duration CLOSE_TIMEOUT = Duration.ofSeconds(5); public static void main(String[] args) { @@ -42,19 +44,26 @@ public static void main(String[] args) { .map(s -> "Echo: " + s) .map(DefaultPayload::create)); - RSocketServer.create(echoAcceptor).bindNow(Http3TransportFactory.server("localhost", 7000)); + CloseableChannel server = + RSocketServer.create(echoAcceptor).bindNow(Http3TransportFactory.server("localhost", 7000)); RSocket socket = RSocketConnector.connectWith(Http3TransportFactory.client("localhost", 7000)).block(); - socket - .requestChannel( - Flux.interval(Duration.ofMillis(1000)).map(i -> DefaultPayload.create("Hello"))) - .map(Payload::getDataUtf8) - .doOnNext(logger::debug) - .take(10) - .doFinally(signalType -> socket.dispose()) - .then() - .block(); + try { + socket + .requestChannel( + Flux.interval(Duration.ofMillis(1000)).map(i -> DefaultPayload.create("Hello"))) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); + } finally { + socket.dispose(); + socket.onClose().block(CLOSE_TIMEOUT); + server.dispose(); + server.onClose().block(CLOSE_TIMEOUT); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/fnf/TaskProcessingWithServerSideNotificationsExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/fnf/TaskProcessingWithServerSideNotificationsExample.java index 0f75a7cb8..8012e8d53 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/fnf/TaskProcessingWithServerSideNotificationsExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/fnf/TaskProcessingWithServerSideNotificationsExample.java @@ -22,6 +22,8 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; import java.util.concurrent.BlockingQueue; @@ -54,8 +56,9 @@ public static void main(String[] args) throws InterruptedException { BackgroundWorker backgroundWorker = new BackgroundWorker(tasksProcessor.asFlux(), idToCompletedTasksMap, idToRSocketMap); - RSocketServer.create(new TasksAcceptor(tasksProcessor, idToCompletedTasksMap, idToRSocketMap)) - .bindNow(Http3TransportFactory.server(9991)); + CloseableChannel server = + RSocketServer.create(new TasksAcceptor(tasksProcessor, idToCompletedTasksMap, idToRSocketMap)) + .bindNow(Http3TransportFactory.server(9991)); Logger logger = LoggerFactory.getLogger("RSocket.Client.ID[Test]"); @@ -71,24 +74,34 @@ public static void main(String[] args) throws InterruptedException { })) .connect(Http3TransportFactory.client(9991)); - RSocket rSocketRequester1 = rSocketMono.block(); + RSocket rSocketRequester1 = null; + RSocket rSocketRequester2 = null; + try { + rSocketRequester1 = rSocketMono.block(); - for (int i = 0; i < 10; i++) { - rSocketRequester1.fireAndForget(DefaultPayload.create("task" + i)).block(); - } + for (int i = 0; i < 10; i++) { + rSocketRequester1.fireAndForget(DefaultPayload.create("task" + i)).block(); + } - Thread.sleep(4000); + Thread.sleep(4000); - rSocketRequester1.dispose(); - logger.info("Disposed"); + ExampleLifecycle.close(rSocketRequester1); + logger.info("Disposed"); - Thread.sleep(4000); + Thread.sleep(4000); - RSocket rSocketRequester2 = rSocketMono.block(); + rSocketRequester2 = rSocketMono.block(); - logger.info("Reconnected"); + logger.info("Reconnected"); - Thread.sleep(10000); + Thread.sleep(10000); + } finally { + ExampleLifecycle.close(rSocketRequester2); + ExampleLifecycle.close(rSocketRequester1); + tasksProcessor.tryEmitComplete(); + backgroundWorker.cancel(); + ExampleLifecycle.close(server); + } } static class BackgroundWorker extends BaseSubscriber { @@ -103,7 +116,6 @@ static class BackgroundWorker extends BaseSubscriber { this.idToCompletedTasksMap = idToCompletedTasksMap; this.idToRSocketMap = idToRSocketMap; - // mimic a long running task processing taskProducer .concatMap( t -> diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/loadbalancer/RoundRobinRSocketLoadbalancerExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/loadbalancer/RoundRobinRSocketLoadbalancerExample.java index 0a86695fe..a03f89c29 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/loadbalancer/RoundRobinRSocketLoadbalancerExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/loadbalancer/RoundRobinRSocketLoadbalancerExample.java @@ -21,6 +21,7 @@ import io.rsocket.loadbalance.LoadbalanceRSocketClient; import io.rsocket.loadbalance.LoadbalanceTarget; import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; @@ -98,12 +99,19 @@ public static void main(String[] args) { RSocketClient rSocketClient = LoadbalanceRSocketClient.builder(producer).roundRobinLoadbalanceStrategy().build(); - for (int i = 0; i < 10000; i++) { - try { - rSocketClient.requestResponse(Mono.just(DefaultPayload.create("test" + i))).block(); - } catch (Throwable t) { - // no ops + try { + for (int i = 0; i < 10000; i++) { + try { + rSocketClient.requestResponse(Mono.just(DefaultPayload.create("test" + i))).block(); + } catch (Throwable t) { + // no ops + } } + } finally { + ExampleLifecycle.close(rSocketClient); + ExampleLifecycle.close(server1); + ExampleLifecycle.close(server2); + ExampleLifecycle.close(server3); } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/CompositeMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/CompositeMetadataExample.java index 9bac418ee..808a1793f 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/CompositeMetadataExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/CompositeMetadataExample.java @@ -30,6 +30,8 @@ import io.rsocket.metadata.TaggingMetadataCodec; import io.rsocket.metadata.WellKnownMimeType; import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.ByteBufPayload; import java.util.Collections; import java.util.Objects; @@ -41,22 +43,24 @@ public class CompositeMetadataExample { static final Logger logger = LoggerFactory.getLogger(CompositeMetadataExample.class); public static void main(String[] args) { - RSocketServer.create( - SocketAcceptor.forRequestResponse( - payload -> { - final String route = decodeRoute(payload.sliceMetadata()); + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); - logger.info("Received RequestResponse[route={}]", route); + logger.info("Received RequestResponse[route={}]", route); - payload.release(); + payload.release(); - if ("my.test.route".equals(route)) { - return Mono.just(ByteBufPayload.create("Hello From My Test Route")); - } + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } - return Mono.error(new IllegalArgumentException("Route " + route + " not found")); - })) - .bindNow(Http3TransportFactory.server("localhost", 7000)); + return Mono.error( + new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(Http3TransportFactory.server("localhost", 7000)); RSocket socket = RSocketConnector.create() @@ -78,12 +82,18 @@ public static void main(String[] args) { WellKnownMimeType.MESSAGE_RSOCKET_ROUTING, routeMetadata); - socket - .requestResponse( - ByteBufPayload.create( - ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), compositeMetadata)) - .log() - .block(); + try { + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), + compositeMetadata)) + .log() + .block(); + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); + } } static String decodeRoute(ByteBuf metadata) { diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/RoutingMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/RoutingMetadataExample.java index 31df59fe8..c00e63885 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/RoutingMetadataExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/metadata/routing/RoutingMetadataExample.java @@ -27,6 +27,8 @@ import io.rsocket.metadata.TaggingMetadataCodec; import io.rsocket.metadata.WellKnownMimeType; import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.ByteBufPayload; import java.util.Collections; import org.slf4j.Logger; @@ -37,22 +39,24 @@ public class RoutingMetadataExample { static final Logger logger = LoggerFactory.getLogger(RoutingMetadataExample.class); public static void main(String[] args) { - RSocketServer.create( - SocketAcceptor.forRequestResponse( - payload -> { - final String route = decodeRoute(payload.sliceMetadata()); + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); - logger.info("Received RequestResponse[route={}]", route); + logger.info("Received RequestResponse[route={}]", route); - payload.release(); + payload.release(); - if ("my.test.route".equals(route)) { - return Mono.just(ByteBufPayload.create("Hello From My Test Route")); - } + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } - return Mono.error(new IllegalArgumentException("Route " + route + " not found")); - })) - .bindNow(Http3TransportFactory.server("localhost", 7000)); + return Mono.error( + new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(Http3TransportFactory.server("localhost", 7000)); RSocket socket = RSocketConnector.create() @@ -66,12 +70,17 @@ public static void main(String[] args) { final ByteBuf routeMetadata = TaggingMetadataCodec.createTaggingContent( ByteBufAllocator.DEFAULT, Collections.singletonList("my.test.route")); - socket - .requestResponse( - ByteBufPayload.create( - ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), routeMetadata)) - .log() - .block(); + try { + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), routeMetadata)) + .log() + .block(); + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); + } } static String decodeRoute(ByteBuf metadata) { diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/plugins/LimitRateInterceptorExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/plugins/LimitRateInterceptorExample.java index f89de7d2b..d24e7f94a 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/plugins/LimitRateInterceptorExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/plugins/LimitRateInterceptorExample.java @@ -5,8 +5,10 @@ import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; -import io.rsocket.plugins.LimitRateInterceptor; import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.plugins.LimitRateInterceptor; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; import org.reactivestreams.Publisher; @@ -19,26 +21,27 @@ public class LimitRateInterceptorExample { private static final Logger logger = LoggerFactory.getLogger(LimitRateInterceptorExample.class); public static void main(String[] args) { - RSocketServer.create( - SocketAcceptor.with( - new RSocket() { - @Override - public Flux requestStream(Payload payload) { - return Flux.interval(Duration.ofMillis(100)) - .doOnRequest( - e -> logger.debug("Server publisher receives request for " + e)) - .map(aLong -> DefaultPayload.create("Interval: " + aLong)); - } + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.with( + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + return Flux.interval(Duration.ofMillis(100)) + .doOnRequest( + e -> logger.debug("Server publisher receives request for " + e)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)); + } - @Override - public Flux requestChannel(Publisher payloads) { - return Flux.from(payloads) - .doOnRequest( - e -> logger.debug("Server publisher receives request for " + e)); - } - })) - .interceptors(registry -> registry.forResponder(LimitRateInterceptor.forResponder(64))) - .bindNow(Http3TransportFactory.server("localhost", 7000)); + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .doOnRequest( + e -> logger.debug("Server publisher receives request for " + e)); + } + })) + .interceptors(registry -> registry.forResponder(LimitRateInterceptor.forResponder(64))) + .bindNow(Http3TransportFactory.server("localhost", 7000)); RSocket socket = RSocketConnector.create() @@ -49,34 +52,37 @@ public Flux requestChannel(Publisher payloads) { logger.debug( "\n\nStart of requestStream interaction\n" + "----------------------------------\n"); - socket - .requestStream(DefaultPayload.create("Hello")) - .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) - .map(Payload::getDataUtf8) - .doOnNext(logger::debug) - .take(10) - .then() - .block(); + try { + socket + .requestStream(DefaultPayload.create("Hello")) + .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); - logger.debug( - "\n\nStart of requestChannel interaction\n" + "-----------------------------------\n"); + logger.debug( + "\n\nStart of requestChannel interaction\n" + "-----------------------------------\n"); - socket - .requestChannel( - Flux.generate( - () -> 1L, - (s, sink) -> { - sink.next(DefaultPayload.create("Next " + s)); - return ++s; - }) - .doOnRequest(e -> logger.debug("Client publisher receives request for " + e))) - .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) - .map(Payload::getDataUtf8) - .doOnNext(logger::debug) - .take(10) - .then() - .doFinally(signalType -> socket.dispose()) - .then() - .block(); + socket + .requestChannel( + Flux.generate( + () -> 1L, + (s, sink) -> { + sink.next(DefaultPayload.create("Next " + s)); + return ++s; + }) + .doOnRequest(e -> logger.debug("Client publisher receives request for " + e))) + .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/requestresponse/HelloWorldClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/requestresponse/HelloWorldClient.java index 02e3839e5..6a2167d15 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/requestresponse/HelloWorldClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/requestresponse/HelloWorldClient.java @@ -22,6 +22,8 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -48,21 +50,25 @@ public Mono requestResponse(Payload p) { } }; - RSocketServer.create(SocketAcceptor.with(rsocket)) - .bindNow(Http3TransportFactory.server("localhost", 7000)); + CloseableChannel server = + RSocketServer.create(SocketAcceptor.with(rsocket)) + .bindNow(Http3TransportFactory.server("localhost", 7000)); RSocket socket = RSocketConnector.connectWith(Http3TransportFactory.client("localhost", 7000)).block(); - for (int i = 0; i < 3; i++) { - socket - .requestResponse(DefaultPayload.create("Hello")) - .map(Payload::getDataUtf8) - .onErrorReturn("error") - .doOnNext(logger::debug) - .block(); + try { + for (int i = 0; i < 3; i++) { + socket + .requestResponse(DefaultPayload.create("Hello")) + .map(Payload::getDataUtf8) + .onErrorReturn("error") + .doOnNext(logger::debug) + .block(); + } + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); } - - socket.dispose(); } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ClientStreamingToServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ClientStreamingToServer.java index e0d84dcd2..e290f7244 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ClientStreamingToServer.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ClientStreamingToServer.java @@ -22,6 +22,8 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; import org.slf4j.Logger; @@ -32,13 +34,14 @@ public final class ClientStreamingToServer { private static final Logger logger = LoggerFactory.getLogger(ClientStreamingToServer.class); - public static void main(String[] args) throws InterruptedException { - RSocketServer.create( - SocketAcceptor.forRequestStream( - payload -> - Flux.interval(Duration.ofMillis(100)) - .map(aLong -> DefaultPayload.create("Interval: " + aLong)))) - .bindNow(Http3TransportFactory.server("localhost", 7000)); + public static void main(String[] args) { + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> + Flux.interval(Duration.ofMillis(100)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)))) + .bindNow(Http3TransportFactory.server("localhost", 7000)); RSocket socket = RSocketConnector.create() @@ -47,16 +50,17 @@ public static void main(String[] args) throws InterruptedException { .block(); final Payload payload = DefaultPayload.create("Hello"); - socket - .requestStream(payload) - .map(Payload::getDataUtf8) - .doOnNext(logger::debug) - .take(10) - .then() - .doFinally(signalType -> socket.dispose()) - .then() - .block(); - - Thread.sleep(1000000); + try { + socket + .requestStream(payload) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ServerStreamingToClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ServerStreamingToClient.java index 33a56494d..0faa38d2f 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ServerStreamingToClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/stream/ServerStreamingToClient.java @@ -23,26 +23,37 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; +import reactor.core.publisher.Sinks; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public final class ServerStreamingToClient { public static void main(String[] args) { + Sinks.Empty completion = Sinks.empty(); - RSocketServer.create( - (setup, rsocket) -> { - rsocket - .requestStream(DefaultPayload.create("Hello-Bidi")) - .map(Payload::getDataUtf8) - .log() - .subscribe(); + CloseableChannel server = + RSocketServer.create( + (setup, rsocket) -> { + rsocket + .requestStream(DefaultPayload.create("Hello-Bidi")) + .map(Payload::getDataUtf8) + .log() + .take(10) + .doFinally( + signalType -> { + rsocket.dispose(); + completion.tryEmitEmpty(); + }) + .subscribe(); - return Mono.just(new RSocket() {}); - }) - .bindNow(Http3TransportFactory.server("localhost", 7000)); + return Mono.just(new RSocket() {}); + }) + .bindNow(Http3TransportFactory.server("localhost", 7000)); RSocket rsocket = RSocketConnector.create() @@ -54,6 +65,11 @@ public static void main(String[] args) { .connect(Http3TransportFactory.client("localhost", 7000)) .block(); - rsocket.onClose().block(); + try { + completion.asMono().block(); + } finally { + ExampleLifecycle.close(rsocket); + ExampleLifecycle.close(server); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/channel/ChannelEchoClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/channel/ChannelEchoClient.java index 94b8bb5e3..d14b63b24 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/channel/ChannelEchoClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/channel/ChannelEchoClient.java @@ -22,6 +22,7 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; import org.slf4j.Logger; @@ -31,6 +32,7 @@ public final class ChannelEchoClient { private static final Logger logger = LoggerFactory.getLogger(ChannelEchoClient.class); + private static final Duration CLOSE_TIMEOUT = Duration.ofSeconds(5); public static void main(String[] args) { @@ -42,19 +44,26 @@ public static void main(String[] args) { .map(s -> "Echo: " + s) .map(DefaultPayload::create)); - RSocketServer.create(echoAcceptor).bindNow(QuicTransportFactory.server("localhost", 7000)); + CloseableChannel server = + RSocketServer.create(echoAcceptor).bindNow(QuicTransportFactory.server("localhost", 7000)); RSocket socket = RSocketConnector.connectWith(QuicTransportFactory.client("localhost", 7000)).block(); - socket - .requestChannel( - Flux.interval(Duration.ofMillis(1000)).map(i -> DefaultPayload.create("Hello"))) - .map(Payload::getDataUtf8) - .doOnNext(logger::debug) - .take(10) - .doFinally(signalType -> socket.dispose()) - .then() - .block(); + try { + socket + .requestChannel( + Flux.interval(Duration.ofMillis(1000)).map(i -> DefaultPayload.create("Hello"))) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); + } finally { + socket.dispose(); + socket.onClose().block(CLOSE_TIMEOUT); + server.dispose(); + server.onClose().block(CLOSE_TIMEOUT); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/fnf/TaskProcessingWithServerSideNotificationsExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/fnf/TaskProcessingWithServerSideNotificationsExample.java index c194dc274..b8d4ff03e 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/fnf/TaskProcessingWithServerSideNotificationsExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/fnf/TaskProcessingWithServerSideNotificationsExample.java @@ -22,6 +22,8 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; import java.util.concurrent.BlockingQueue; @@ -54,8 +56,9 @@ public static void main(String[] args) throws InterruptedException { BackgroundWorker backgroundWorker = new BackgroundWorker(tasksProcessor.asFlux(), idToCompletedTasksMap, idToRSocketMap); - RSocketServer.create(new TasksAcceptor(tasksProcessor, idToCompletedTasksMap, idToRSocketMap)) - .bindNow(QuicTransportFactory.server(9991)); + CloseableChannel server = + RSocketServer.create(new TasksAcceptor(tasksProcessor, idToCompletedTasksMap, idToRSocketMap)) + .bindNow(QuicTransportFactory.server(9991)); Logger logger = LoggerFactory.getLogger("RSocket.Client.ID[Test]"); @@ -71,24 +74,34 @@ public static void main(String[] args) throws InterruptedException { })) .connect(QuicTransportFactory.client(9991)); - RSocket rSocketRequester1 = rSocketMono.block(); + RSocket rSocketRequester1 = null; + RSocket rSocketRequester2 = null; + try { + rSocketRequester1 = rSocketMono.block(); - for (int i = 0; i < 10; i++) { - rSocketRequester1.fireAndForget(DefaultPayload.create("task" + i)).block(); - } + for (int i = 0; i < 10; i++) { + rSocketRequester1.fireAndForget(DefaultPayload.create("task" + i)).block(); + } - Thread.sleep(4000); + Thread.sleep(4000); - rSocketRequester1.dispose(); - logger.info("Disposed"); + ExampleLifecycle.close(rSocketRequester1); + logger.info("Disposed"); - Thread.sleep(4000); + Thread.sleep(4000); - RSocket rSocketRequester2 = rSocketMono.block(); + rSocketRequester2 = rSocketMono.block(); - logger.info("Reconnected"); + logger.info("Reconnected"); - Thread.sleep(10000); + Thread.sleep(10000); + } finally { + ExampleLifecycle.close(rSocketRequester2); + ExampleLifecycle.close(rSocketRequester1); + tasksProcessor.tryEmitComplete(); + backgroundWorker.cancel(); + ExampleLifecycle.close(server); + } } static class BackgroundWorker extends BaseSubscriber { @@ -103,7 +116,6 @@ static class BackgroundWorker extends BaseSubscriber { this.idToCompletedTasksMap = idToCompletedTasksMap; this.idToRSocketMap = idToRSocketMap; - // mimic a long running task processing taskProducer .concatMap( t -> diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/loadbalancer/RoundRobinRSocketLoadbalancerExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/loadbalancer/RoundRobinRSocketLoadbalancerExample.java index 13e290f1d..daa750b0d 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/loadbalancer/RoundRobinRSocketLoadbalancerExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/loadbalancer/RoundRobinRSocketLoadbalancerExample.java @@ -21,6 +21,7 @@ import io.rsocket.loadbalance.LoadbalanceRSocketClient; import io.rsocket.loadbalance.LoadbalanceTarget; import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; @@ -98,12 +99,19 @@ public static void main(String[] args) { RSocketClient rSocketClient = LoadbalanceRSocketClient.builder(producer).roundRobinLoadbalanceStrategy().build(); - for (int i = 0; i < 10000; i++) { - try { - rSocketClient.requestResponse(Mono.just(DefaultPayload.create("test" + i))).block(); - } catch (Throwable t) { - // no ops + try { + for (int i = 0; i < 10000; i++) { + try { + rSocketClient.requestResponse(Mono.just(DefaultPayload.create("test" + i))).block(); + } catch (Throwable t) { + // no ops + } } + } finally { + ExampleLifecycle.close(rSocketClient); + ExampleLifecycle.close(server1); + ExampleLifecycle.close(server2); + ExampleLifecycle.close(server3); } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/CompositeMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/CompositeMetadataExample.java index f1f6cf6dd..71d09a548 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/CompositeMetadataExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/CompositeMetadataExample.java @@ -30,6 +30,8 @@ import io.rsocket.metadata.TaggingMetadataCodec; import io.rsocket.metadata.WellKnownMimeType; import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.ByteBufPayload; import java.util.Collections; import java.util.Objects; @@ -41,22 +43,24 @@ public class CompositeMetadataExample { static final Logger logger = LoggerFactory.getLogger(CompositeMetadataExample.class); public static void main(String[] args) { - RSocketServer.create( - SocketAcceptor.forRequestResponse( - payload -> { - final String route = decodeRoute(payload.sliceMetadata()); + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); - logger.info("Received RequestResponse[route={}]", route); + logger.info("Received RequestResponse[route={}]", route); - payload.release(); + payload.release(); - if ("my.test.route".equals(route)) { - return Mono.just(ByteBufPayload.create("Hello From My Test Route")); - } + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } - return Mono.error(new IllegalArgumentException("Route " + route + " not found")); - })) - .bindNow(QuicTransportFactory.server("localhost", 7000)); + return Mono.error( + new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(QuicTransportFactory.server("localhost", 7000)); RSocket socket = RSocketConnector.create() @@ -78,12 +82,18 @@ public static void main(String[] args) { WellKnownMimeType.MESSAGE_RSOCKET_ROUTING, routeMetadata); - socket - .requestResponse( - ByteBufPayload.create( - ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), compositeMetadata)) - .log() - .block(); + try { + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), + compositeMetadata)) + .log() + .block(); + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); + } } static String decodeRoute(ByteBuf metadata) { diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/RoutingMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/RoutingMetadataExample.java index 764433ed3..482956e9a 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/RoutingMetadataExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/metadata/routing/RoutingMetadataExample.java @@ -27,6 +27,8 @@ import io.rsocket.metadata.TaggingMetadataCodec; import io.rsocket.metadata.WellKnownMimeType; import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.ByteBufPayload; import java.util.Collections; import org.slf4j.Logger; @@ -37,22 +39,24 @@ public class RoutingMetadataExample { static final Logger logger = LoggerFactory.getLogger(RoutingMetadataExample.class); public static void main(String[] args) { - RSocketServer.create( - SocketAcceptor.forRequestResponse( - payload -> { - final String route = decodeRoute(payload.sliceMetadata()); + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); - logger.info("Received RequestResponse[route={}]", route); + logger.info("Received RequestResponse[route={}]", route); - payload.release(); + payload.release(); - if ("my.test.route".equals(route)) { - return Mono.just(ByteBufPayload.create("Hello From My Test Route")); - } + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } - return Mono.error(new IllegalArgumentException("Route " + route + " not found")); - })) - .bindNow(QuicTransportFactory.server("localhost", 7000)); + return Mono.error( + new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(QuicTransportFactory.server("localhost", 7000)); RSocket socket = RSocketConnector.create() @@ -66,12 +70,17 @@ public static void main(String[] args) { final ByteBuf routeMetadata = TaggingMetadataCodec.createTaggingContent( ByteBufAllocator.DEFAULT, Collections.singletonList("my.test.route")); - socket - .requestResponse( - ByteBufPayload.create( - ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), routeMetadata)) - .log() - .block(); + try { + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), routeMetadata)) + .log() + .block(); + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); + } } static String decodeRoute(ByteBuf metadata) { diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/plugins/LimitRateInterceptorExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/plugins/LimitRateInterceptorExample.java index 9e53d80cb..8ac6ab921 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/plugins/LimitRateInterceptorExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/plugins/LimitRateInterceptorExample.java @@ -5,8 +5,10 @@ import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; -import io.rsocket.plugins.LimitRateInterceptor; import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.plugins.LimitRateInterceptor; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; import org.reactivestreams.Publisher; @@ -19,26 +21,27 @@ public class LimitRateInterceptorExample { private static final Logger logger = LoggerFactory.getLogger(LimitRateInterceptorExample.class); public static void main(String[] args) { - RSocketServer.create( - SocketAcceptor.with( - new RSocket() { - @Override - public Flux requestStream(Payload payload) { - return Flux.interval(Duration.ofMillis(100)) - .doOnRequest( - e -> logger.debug("Server publisher receives request for " + e)) - .map(aLong -> DefaultPayload.create("Interval: " + aLong)); - } + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.with( + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + return Flux.interval(Duration.ofMillis(100)) + .doOnRequest( + e -> logger.debug("Server publisher receives request for " + e)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)); + } - @Override - public Flux requestChannel(Publisher payloads) { - return Flux.from(payloads) - .doOnRequest( - e -> logger.debug("Server publisher receives request for " + e)); - } - })) - .interceptors(registry -> registry.forResponder(LimitRateInterceptor.forResponder(64))) - .bindNow(QuicTransportFactory.server("localhost", 7000)); + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .doOnRequest( + e -> logger.debug("Server publisher receives request for " + e)); + } + })) + .interceptors(registry -> registry.forResponder(LimitRateInterceptor.forResponder(64))) + .bindNow(QuicTransportFactory.server("localhost", 7000)); RSocket socket = RSocketConnector.create() @@ -49,34 +52,37 @@ public Flux requestChannel(Publisher payloads) { logger.debug( "\n\nStart of requestStream interaction\n" + "----------------------------------\n"); - socket - .requestStream(DefaultPayload.create("Hello")) - .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) - .map(Payload::getDataUtf8) - .doOnNext(logger::debug) - .take(10) - .then() - .block(); + try { + socket + .requestStream(DefaultPayload.create("Hello")) + .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); - logger.debug( - "\n\nStart of requestChannel interaction\n" + "-----------------------------------\n"); + logger.debug( + "\n\nStart of requestChannel interaction\n" + "-----------------------------------\n"); - socket - .requestChannel( - Flux.generate( - () -> 1L, - (s, sink) -> { - sink.next(DefaultPayload.create("Next " + s)); - return ++s; - }) - .doOnRequest(e -> logger.debug("Client publisher receives request for " + e))) - .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) - .map(Payload::getDataUtf8) - .doOnNext(logger::debug) - .take(10) - .then() - .doFinally(signalType -> socket.dispose()) - .then() - .block(); + socket + .requestChannel( + Flux.generate( + () -> 1L, + (s, sink) -> { + sink.next(DefaultPayload.create("Next " + s)); + return ++s; + }) + .doOnRequest(e -> logger.debug("Client publisher receives request for " + e))) + .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/requestresponse/HelloWorldClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/requestresponse/HelloWorldClient.java index ffc2e7a64..c49a60216 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/requestresponse/HelloWorldClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/requestresponse/HelloWorldClient.java @@ -22,6 +22,7 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import org.slf4j.Logger; @@ -58,16 +59,18 @@ public Mono requestResponse(Payload p) { QuicTransportFactory.client("127.0.0.1", server.address().getPort())) .block(); - for (int i = 0; i < 3; i++) { - socket - .requestResponse(DefaultPayload.create("Hello")) - .map(Payload::getDataUtf8) - .onErrorReturn("error") - .doOnNext(logger::debug) - .block(); + try { + for (int i = 0; i < 3; i++) { + socket + .requestResponse(DefaultPayload.create("Hello")) + .map(Payload::getDataUtf8) + .onErrorReturn("error") + .doOnNext(logger::debug) + .block(); + } + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); } - - socket.dispose(); - server.dispose(); } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ClientStreamingToServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ClientStreamingToServer.java index d2f426867..9bde7a516 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ClientStreamingToServer.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ClientStreamingToServer.java @@ -22,6 +22,8 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; import org.slf4j.Logger; @@ -32,13 +34,14 @@ public final class ClientStreamingToServer { private static final Logger logger = LoggerFactory.getLogger(ClientStreamingToServer.class); - public static void main(String[] args) throws InterruptedException { - RSocketServer.create( - SocketAcceptor.forRequestStream( - payload -> - Flux.interval(Duration.ofMillis(100)) - .map(aLong -> DefaultPayload.create("Interval: " + aLong)))) - .bindNow(QuicTransportFactory.server("localhost", 7000)); + public static void main(String[] args) { + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> + Flux.interval(Duration.ofMillis(100)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)))) + .bindNow(QuicTransportFactory.server("localhost", 7000)); RSocket socket = RSocketConnector.create() @@ -47,16 +50,17 @@ public static void main(String[] args) throws InterruptedException { .block(); final Payload payload = DefaultPayload.create("Hello"); - socket - .requestStream(payload) - .map(Payload::getDataUtf8) - .doOnNext(logger::debug) - .take(10) - .then() - .doFinally(signalType -> socket.dispose()) - .then() - .block(); - - Thread.sleep(1000000); + try { + socket + .requestStream(payload) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ServerStreamingToClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ServerStreamingToClient.java index f5796de37..c933ff27d 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ServerStreamingToClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/stream/ServerStreamingToClient.java @@ -23,26 +23,37 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; +import reactor.core.publisher.Sinks; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public final class ServerStreamingToClient { public static void main(String[] args) { + Sinks.Empty completion = Sinks.empty(); - RSocketServer.create( - (setup, rsocket) -> { - rsocket - .requestStream(DefaultPayload.create("Hello-Bidi")) - .map(Payload::getDataUtf8) - .log() - .subscribe(); + CloseableChannel server = + RSocketServer.create( + (setup, rsocket) -> { + rsocket + .requestStream(DefaultPayload.create("Hello-Bidi")) + .map(Payload::getDataUtf8) + .log() + .take(10) + .doFinally( + signalType -> { + rsocket.dispose(); + completion.tryEmitEmpty(); + }) + .subscribe(); - return Mono.just(new RSocket() {}); - }) - .bindNow(QuicTransportFactory.server("localhost", 7000)); + return Mono.just(new RSocket() {}); + }) + .bindNow(QuicTransportFactory.server("localhost", 7000)); RSocket rsocket = RSocketConnector.create() @@ -54,6 +65,11 @@ public static void main(String[] args) { .connect(QuicTransportFactory.client("localhost", 7000)) .block(); - rsocket.onClose().block(); + try { + completion.asMono().block(); + } finally { + ExampleLifecycle.close(rsocket); + ExampleLifecycle.close(server); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ExampleLifecycle.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ExampleLifecycle.java new file mode 100644 index 000000000..41a8a33d2 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/support/ExampleLifecycle.java @@ -0,0 +1,59 @@ +/* + * Copyright 2015-2026 the original author or 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.rsocket.examples.transport.support; + +import io.rsocket.Closeable; +import java.time.Duration; +import reactor.core.publisher.Mono; +import reactor.netty.DisposableServer; + +public final class ExampleLifecycle { + + private static final Duration CLOSE_TIMEOUT = Duration.ofSeconds(5); + + private ExampleLifecycle() {} + + public static void close(Closeable closeable) { + if (closeable == null) { + return; + } + + closeable.dispose(); + closeable.onClose().onErrorResume(__ -> Mono.empty()).block(CLOSE_TIMEOUT); + } + + public static void close(DisposableServer server) { + if (server == null) { + return; + } + + server.disposeNow(CLOSE_TIMEOUT); + } + + public static void interrupt(Thread thread) { + if (thread == null) { + return; + } + + thread.interrupt(); + try { + thread.join(CLOSE_TIMEOUT.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/channel/ChannelEchoClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/channel/ChannelEchoClient.java index 463043020..aa609fd47 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/channel/ChannelEchoClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/channel/ChannelEchoClient.java @@ -22,6 +22,7 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; import java.time.Duration; @@ -32,6 +33,7 @@ public final class ChannelEchoClient { private static final Logger logger = LoggerFactory.getLogger(ChannelEchoClient.class); + private static final Duration CLOSE_TIMEOUT = Duration.ofSeconds(5); public static void main(String[] args) { @@ -43,19 +45,26 @@ public static void main(String[] args) { .map(s -> "Echo: " + s) .map(DefaultPayload::create)); - RSocketServer.create(echoAcceptor).bindNow(TcpServerTransport.create("localhost", 7000)); + CloseableChannel server = + RSocketServer.create(echoAcceptor).bindNow(TcpServerTransport.create("localhost", 7000)); RSocket socket = RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); - socket - .requestChannel( - Flux.interval(Duration.ofMillis(1000)).map(i -> DefaultPayload.create("Hello"))) - .map(Payload::getDataUtf8) - .doOnNext(logger::debug) - .take(10) - .doFinally(signalType -> socket.dispose()) - .then() - .block(); + try { + socket + .requestChannel( + Flux.interval(Duration.ofMillis(1000)).map(i -> DefaultPayload.create("Hello"))) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); + } finally { + socket.dispose(); + socket.onClose().block(CLOSE_TIMEOUT); + server.dispose(); + server.onClose().block(CLOSE_TIMEOUT); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java index 89b22749f..0b994a116 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/fnf/TaskProcessingWithServerSideNotificationsExample.java @@ -21,7 +21,9 @@ import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; import java.time.Duration; @@ -55,8 +57,9 @@ public static void main(String[] args) throws InterruptedException { BackgroundWorker backgroundWorker = new BackgroundWorker(tasksProcessor.asFlux(), idToCompletedTasksMap, idToRSocketMap); - RSocketServer.create(new TasksAcceptor(tasksProcessor, idToCompletedTasksMap, idToRSocketMap)) - .bindNow(TcpServerTransport.create(9991)); + CloseableChannel server = + RSocketServer.create(new TasksAcceptor(tasksProcessor, idToCompletedTasksMap, idToRSocketMap)) + .bindNow(TcpServerTransport.create(9991)); Logger logger = LoggerFactory.getLogger("RSocket.Client.ID[Test]"); @@ -72,24 +75,34 @@ public static void main(String[] args) throws InterruptedException { })) .connect(TcpClientTransport.create(9991)); - RSocket rSocketRequester1 = rSocketMono.block(); + RSocket rSocketRequester1 = null; + RSocket rSocketRequester2 = null; + try { + rSocketRequester1 = rSocketMono.block(); - for (int i = 0; i < 10; i++) { - rSocketRequester1.fireAndForget(DefaultPayload.create("task" + i)).block(); - } + for (int i = 0; i < 10; i++) { + rSocketRequester1.fireAndForget(DefaultPayload.create("task" + i)).block(); + } - Thread.sleep(4000); + Thread.sleep(4000); - rSocketRequester1.dispose(); - logger.info("Disposed"); + ExampleLifecycle.close(rSocketRequester1); + logger.info("Disposed"); - Thread.sleep(4000); + Thread.sleep(4000); - RSocket rSocketRequester2 = rSocketMono.block(); + rSocketRequester2 = rSocketMono.block(); - logger.info("Reconnected"); + logger.info("Reconnected"); - Thread.sleep(10000); + Thread.sleep(10000); + } finally { + ExampleLifecycle.close(rSocketRequester2); + ExampleLifecycle.close(rSocketRequester1); + tasksProcessor.tryEmitComplete(); + backgroundWorker.cancel(); + ExampleLifecycle.close(server); + } } static class BackgroundWorker extends BaseSubscriber { @@ -104,7 +117,6 @@ static class BackgroundWorker extends BaseSubscriber { this.idToCompletedTasksMap = idToCompletedTasksMap; this.idToRSocketMap = idToRSocketMap; - // mimic a long running task processing taskProducer .concatMap( t -> diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java index abed4a52d..28be358ce 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/loadbalancer/RoundRobinRSocketLoadbalancerExample.java @@ -18,6 +18,7 @@ import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketClient; import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.loadbalance.LoadbalanceRSocketClient; import io.rsocket.loadbalance.LoadbalanceTarget; import io.rsocket.transport.netty.client.TcpClientTransport; @@ -99,12 +100,19 @@ public static void main(String[] args) { RSocketClient rSocketClient = LoadbalanceRSocketClient.builder(producer).roundRobinLoadbalanceStrategy().build(); - for (int i = 0; i < 10000; i++) { - try { - rSocketClient.requestResponse(Mono.just(DefaultPayload.create("test" + i))).block(); - } catch (Throwable t) { - // no ops + try { + for (int i = 0; i < 10000; i++) { + try { + rSocketClient.requestResponse(Mono.just(DefaultPayload.create("test" + i))).block(); + } catch (Throwable t) { + // no ops + } } + } finally { + ExampleLifecycle.close(rSocketClient); + ExampleLifecycle.close(server1); + ExampleLifecycle.close(server2); + ExampleLifecycle.close(server3); } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/CompositeMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/CompositeMetadataExample.java index a0a02a946..74abebfea 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/CompositeMetadataExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/CompositeMetadataExample.java @@ -24,12 +24,14 @@ import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.metadata.CompositeMetadata; import io.rsocket.metadata.CompositeMetadataCodec; import io.rsocket.metadata.RoutingMetadata; import io.rsocket.metadata.TaggingMetadataCodec; import io.rsocket.metadata.WellKnownMimeType; import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.ByteBufPayload; import java.util.Collections; @@ -42,22 +44,24 @@ public class CompositeMetadataExample { static final Logger logger = LoggerFactory.getLogger(CompositeMetadataExample.class); public static void main(String[] args) { - RSocketServer.create( - SocketAcceptor.forRequestResponse( - payload -> { - final String route = decodeRoute(payload.sliceMetadata()); + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); - logger.info("Received RequestResponse[route={}]", route); + logger.info("Received RequestResponse[route={}]", route); - payload.release(); + payload.release(); - if ("my.test.route".equals(route)) { - return Mono.just(ByteBufPayload.create("Hello From My Test Route")); - } + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } - return Mono.error(new IllegalArgumentException("Route " + route + " not found")); - })) - .bindNow(TcpServerTransport.create("localhost", 7000)); + return Mono.error( + new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(TcpServerTransport.create("localhost", 7000)); RSocket socket = RSocketConnector.create() @@ -79,12 +83,18 @@ public static void main(String[] args) { WellKnownMimeType.MESSAGE_RSOCKET_ROUTING, routeMetadata); - socket - .requestResponse( - ByteBufPayload.create( - ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), compositeMetadata)) - .log() - .block(); + try { + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), + compositeMetadata)) + .log() + .block(); + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); + } } static String decodeRoute(ByteBuf metadata) { diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/RoutingMetadataExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/RoutingMetadataExample.java index 2aee18bf9..0bd741157 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/RoutingMetadataExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/metadata/routing/RoutingMetadataExample.java @@ -23,10 +23,12 @@ import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.metadata.RoutingMetadata; import io.rsocket.metadata.TaggingMetadataCodec; import io.rsocket.metadata.WellKnownMimeType; import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.ByteBufPayload; import java.util.Collections; @@ -38,22 +40,24 @@ public class RoutingMetadataExample { static final Logger logger = LoggerFactory.getLogger(RoutingMetadataExample.class); public static void main(String[] args) { - RSocketServer.create( - SocketAcceptor.forRequestResponse( - payload -> { - final String route = decodeRoute(payload.sliceMetadata()); + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + payload -> { + final String route = decodeRoute(payload.sliceMetadata()); - logger.info("Received RequestResponse[route={}]", route); + logger.info("Received RequestResponse[route={}]", route); - payload.release(); + payload.release(); - if ("my.test.route".equals(route)) { - return Mono.just(ByteBufPayload.create("Hello From My Test Route")); - } + if ("my.test.route".equals(route)) { + return Mono.just(ByteBufPayload.create("Hello From My Test Route")); + } - return Mono.error(new IllegalArgumentException("Route " + route + " not found")); - })) - .bindNow(TcpServerTransport.create("localhost", 7000)); + return Mono.error( + new IllegalArgumentException("Route " + route + " not found")); + })) + .bindNow(TcpServerTransport.create("localhost", 7000)); RSocket socket = RSocketConnector.create() @@ -67,12 +71,17 @@ public static void main(String[] args) { final ByteBuf routeMetadata = TaggingMetadataCodec.createTaggingContent( ByteBufAllocator.DEFAULT, Collections.singletonList("my.test.route")); - socket - .requestResponse( - ByteBufPayload.create( - ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), routeMetadata)) - .log() - .block(); + try { + socket + .requestResponse( + ByteBufPayload.create( + ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "HelloWorld"), routeMetadata)) + .log() + .block(); + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); + } } static String decodeRoute(ByteBuf metadata) { diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/plugins/LimitRateInterceptorExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/plugins/LimitRateInterceptorExample.java index 5491a1aab..4e84e5535 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/plugins/LimitRateInterceptorExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/plugins/LimitRateInterceptorExample.java @@ -5,8 +5,10 @@ import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.plugins.LimitRateInterceptor; import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; import java.time.Duration; @@ -20,26 +22,27 @@ public class LimitRateInterceptorExample { private static final Logger logger = LoggerFactory.getLogger(LimitRateInterceptorExample.class); public static void main(String[] args) { - RSocketServer.create( - SocketAcceptor.with( - new RSocket() { - @Override - public Flux requestStream(Payload payload) { - return Flux.interval(Duration.ofMillis(100)) - .doOnRequest( - e -> logger.debug("Server publisher receives request for " + e)) - .map(aLong -> DefaultPayload.create("Interval: " + aLong)); - } + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.with( + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + return Flux.interval(Duration.ofMillis(100)) + .doOnRequest( + e -> logger.debug("Server publisher receives request for " + e)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)); + } - @Override - public Flux requestChannel(Publisher payloads) { - return Flux.from(payloads) - .doOnRequest( - e -> logger.debug("Server publisher receives request for " + e)); - } - })) - .interceptors(registry -> registry.forResponder(LimitRateInterceptor.forResponder(64))) - .bindNow(TcpServerTransport.create("localhost", 7000)); + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .doOnRequest( + e -> logger.debug("Server publisher receives request for " + e)); + } + })) + .interceptors(registry -> registry.forResponder(LimitRateInterceptor.forResponder(64))) + .bindNow(TcpServerTransport.create("localhost", 7000)); RSocket socket = RSocketConnector.create() @@ -50,34 +53,37 @@ public Flux requestChannel(Publisher payloads) { logger.debug( "\n\nStart of requestStream interaction\n" + "----------------------------------\n"); - socket - .requestStream(DefaultPayload.create("Hello")) - .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) - .map(Payload::getDataUtf8) - .doOnNext(logger::debug) - .take(10) - .then() - .block(); + try { + socket + .requestStream(DefaultPayload.create("Hello")) + .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); - logger.debug( - "\n\nStart of requestChannel interaction\n" + "-----------------------------------\n"); + logger.debug( + "\n\nStart of requestChannel interaction\n" + "-----------------------------------\n"); - socket - .requestChannel( - Flux.generate( - () -> 1L, - (s, sink) -> { - sink.next(DefaultPayload.create("Next " + s)); - return ++s; - }) - .doOnRequest(e -> logger.debug("Client publisher receives request for " + e))) - .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) - .map(Payload::getDataUtf8) - .doOnNext(logger::debug) - .take(10) - .then() - .doFinally(signalType -> socket.dispose()) - .then() - .block(); + socket + .requestChannel( + Flux.generate( + () -> 1L, + (s, sink) -> { + sink.next(DefaultPayload.create("Next " + s)); + return ++s; + }) + .doOnRequest(e -> logger.debug("Client publisher receives request for " + e))) + .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/requestresponse/HelloWorldClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/requestresponse/HelloWorldClient.java index 0c372d2d8..f26c503f6 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/requestresponse/HelloWorldClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/requestresponse/HelloWorldClient.java @@ -21,7 +21,9 @@ import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; import org.slf4j.Logger; @@ -49,21 +51,25 @@ public Mono requestResponse(Payload p) { } }; - RSocketServer.create(SocketAcceptor.with(rsocket)) - .bindNow(TcpServerTransport.create("localhost", 7000)); + CloseableChannel server = + RSocketServer.create(SocketAcceptor.with(rsocket)) + .bindNow(TcpServerTransport.create("localhost", 7000)); RSocket socket = RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); - for (int i = 0; i < 3; i++) { - socket - .requestResponse(DefaultPayload.create("Hello")) - .map(Payload::getDataUtf8) - .onErrorReturn("error") - .doOnNext(logger::debug) - .block(); + try { + for (int i = 0; i < 3; i++) { + socket + .requestResponse(DefaultPayload.create("Hello")) + .map(Payload::getDataUtf8) + .onErrorReturn("error") + .doOnNext(logger::debug) + .block(); + } + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); } - - socket.dispose(); } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java index af0df3be1..250fd3e19 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ClientStreamingToServer.java @@ -21,7 +21,9 @@ import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; import java.time.Duration; @@ -33,13 +35,14 @@ public final class ClientStreamingToServer { private static final Logger logger = LoggerFactory.getLogger(ClientStreamingToServer.class); - public static void main(String[] args) throws InterruptedException { - RSocketServer.create( - SocketAcceptor.forRequestStream( - payload -> - Flux.interval(Duration.ofMillis(100)) - .map(aLong -> DefaultPayload.create("Interval: " + aLong)))) - .bindNow(TcpServerTransport.create("localhost", 7000)); + public static void main(String[] args) { + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> + Flux.interval(Duration.ofMillis(100)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)))) + .bindNow(TcpServerTransport.create("localhost", 7000)); RSocket socket = RSocketConnector.create() @@ -48,16 +51,17 @@ public static void main(String[] args) throws InterruptedException { .block(); final Payload payload = DefaultPayload.create("Hello"); - socket - .requestStream(payload) - .map(Payload::getDataUtf8) - .doOnNext(logger::debug) - .take(10) - .then() - .doFinally(signalType -> socket.dispose()) - .then() - .block(); - - Thread.sleep(1000000); + try { + socket + .requestStream(payload) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); + } finally { + ExampleLifecycle.close(socket); + ExampleLifecycle.close(server); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ServerStreamingToClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ServerStreamingToClient.java index 10ed34553..a63b610a2 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ServerStreamingToClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/ServerStreamingToClient.java @@ -22,28 +22,39 @@ import io.rsocket.RSocket; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; import java.time.Duration; +import reactor.core.publisher.Sinks; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public final class ServerStreamingToClient { public static void main(String[] args) { + Sinks.Empty completion = Sinks.empty(); - RSocketServer.create( - (setup, rsocket) -> { - rsocket - .requestStream(DefaultPayload.create("Hello-Bidi")) - .map(Payload::getDataUtf8) - .log() - .subscribe(); + CloseableChannel server = + RSocketServer.create( + (setup, rsocket) -> { + rsocket + .requestStream(DefaultPayload.create("Hello-Bidi")) + .map(Payload::getDataUtf8) + .log() + .take(10) + .doFinally( + signalType -> { + rsocket.dispose(); + completion.tryEmitEmpty(); + }) + .subscribe(); - return Mono.just(new RSocket() {}); - }) - .bindNow(TcpServerTransport.create("localhost", 7000)); + return Mono.just(new RSocket() {}); + }) + .bindNow(TcpServerTransport.create("localhost", 7000)); RSocket rsocket = RSocketConnector.create() @@ -55,6 +66,11 @@ public static void main(String[] args) { .connect(TcpClientTransport.create("localhost", 7000)) .block(); - rsocket.onClose().block(); + try { + completion.asMono().block(); + } finally { + ExampleLifecycle.close(rsocket); + ExampleLifecycle.close(server); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketAggregationSample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketAggregationSample.java index 89304853c..84b1298e5 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketAggregationSample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketAggregationSample.java @@ -20,6 +20,7 @@ import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.WebsocketDuplexConnection; @@ -70,11 +71,14 @@ public static void main(String[] args) { .connect(transport) .block(); - Flux.range(1, 100) - .concatMap(i -> clientRSocket.requestResponse(ByteBufPayload.create("Hello " + i))) - .doOnNext(payload -> logger.debug("Processed " + payload.getDataUtf8())) - .blockLast(); - clientRSocket.dispose(); - server.dispose(); + try { + Flux.range(1, 100) + .concatMap(i -> clientRSocket.requestResponse(ByteBufPayload.create("Hello " + i))) + .doOnNext(payload -> logger.debug("Processed " + payload.getDataUtf8())) + .blockLast(); + } finally { + ExampleLifecycle.close(clientRSocket); + ExampleLifecycle.close(server); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketHeadersSample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketHeadersSample.java index 72e003d2a..0d84347fc 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketHeadersSample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketHeadersSample.java @@ -21,6 +21,7 @@ import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.WebsocketDuplexConnection; @@ -81,19 +82,28 @@ public static void main(String[] args) { .connect(transport) .block(); - Flux.range(1, 100) - .concatMap(i -> clientRSocket.requestResponse(ByteBufPayload.create("Hello " + i))) - .doOnNext(payload -> logger.debug("Processed " + payload.getDataUtf8())) - .blockLast(); - clientRSocket.dispose(); + try { + Flux.range(1, 100) + .concatMap(i -> clientRSocket.requestResponse(ByteBufPayload.create("Hello " + i))) + .doOnNext(payload -> logger.debug("Processed " + payload.getDataUtf8())) + .blockLast(); + ExampleLifecycle.close(clientRSocket); - logger.debug( - "\n\nStart of Unauthorized WebSocket Upgrade\n----------------------------------\n"); + logger.debug( + "\n\nStart of Unauthorized WebSocket Upgrade\n----------------------------------\n"); - RSocketConnector.create() - .keepAlive(Duration.ofMinutes(10), Duration.ofMinutes(10)) - .payloadDecoder(PayloadDecoder.ZERO_COPY) - .connect(WebsocketClientTransport.create(server.host(), server.port())) - .block(); + try { + RSocketConnector.create() + .keepAlive(Duration.ofMinutes(10), Duration.ofMinutes(10)) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(WebsocketClientTransport.create(server.host(), server.port())) + .block(); + } catch (Throwable t) { + logger.debug("Unauthorized WebSocket upgrade rejected as expected"); + } + } finally { + ExampleLifecycle.close(clientRSocket); + ExampleLifecycle.close(server); + } } } From 27025e21850d1cfefd96ffb0d33dc460a083dea4 Mon Sep 17 00:00:00 2001 From: "jeroen.veltman" Date: Thu, 2 Apr 2026 22:08:37 +0200 Subject: [PATCH 11/11] Shut down RSocket client examples cleanly Signed-off-by: jeroen.veltman --- .../h3/client/RSocketClientExample.java | 56 +++++++++++-------- .../quic/client/RSocketClientExample.java | 56 +++++++++++-------- .../tcp/client/RSocketClientExample.java | 56 +++++++++++-------- 3 files changed, 99 insertions(+), 69 deletions(-) diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/client/RSocketClientExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/client/RSocketClientExample.java index 4a702379b..ac76b2828 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/client/RSocketClientExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/h3/client/RSocketClientExample.java @@ -7,6 +7,8 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.examples.transport.h3.Http3TransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; import org.slf4j.Logger; @@ -19,36 +21,44 @@ public class RSocketClientExample { public static void main(String[] args) { - RSocketServer.create( - SocketAcceptor.forRequestResponse( - p -> { - String data = p.getDataUtf8(); - logger.info("Received request data {}", data); + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + p -> { + String data = p.getDataUtf8(); + logger.info("Received request data {}", data); - Payload responsePayload = DefaultPayload.create("Echo: " + data); - p.release(); + Payload responsePayload = DefaultPayload.create("Echo: " + data); + p.release(); - return Mono.just(responsePayload); - })) - .bind(Http3TransportFactory.server("localhost", 7000)) - .delaySubscription(Duration.ofSeconds(5)) - .doOnNext(cc -> logger.info("Server started on the address : {}", cc.address())) - .block(); + return Mono.just(responsePayload); + })) + .bind(Http3TransportFactory.server("localhost", 7000)) + .delaySubscription(Duration.ofSeconds(5)) + .doOnNext(cc -> logger.info("Server started on the address : {}", cc.address())) + .block(); Mono source = RSocketConnector.create() .reconnect(Retry.backoff(50, Duration.ofMillis(500))) .connect(Http3TransportFactory.client("localhost", 7000)); - RSocketClient.from(source) - .requestResponse(Mono.just(DefaultPayload.create("Test Request"))) - .doOnSubscribe(s -> logger.info("Executing Request")) - .doOnNext( - d -> { - logger.info("Received response data {}", d.getDataUtf8()); - d.release(); - }) - .repeat(10) - .blockLast(); + RSocketClient client = RSocketClient.from(source); + + try { + client + .requestResponse(Mono.just(DefaultPayload.create("Test Request"))) + .doOnSubscribe(s -> logger.info("Executing Request")) + .doOnNext( + d -> { + logger.info("Received response data {}", d.getDataUtf8()); + d.release(); + }) + .repeat(10) + .blockLast(); + } finally { + ExampleLifecycle.close(client); + ExampleLifecycle.close(server); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/client/RSocketClientExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/client/RSocketClientExample.java index 66ff4fb73..b34a64af3 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/client/RSocketClientExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/quic/client/RSocketClientExample.java @@ -7,6 +7,8 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.examples.transport.quic.QuicTransportFactory; +import io.rsocket.examples.transport.support.ExampleLifecycle; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.util.DefaultPayload; import java.time.Duration; import org.slf4j.Logger; @@ -19,36 +21,44 @@ public class RSocketClientExample { public static void main(String[] args) { - RSocketServer.create( - SocketAcceptor.forRequestResponse( - p -> { - String data = p.getDataUtf8(); - logger.info("Received request data {}", data); + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + p -> { + String data = p.getDataUtf8(); + logger.info("Received request data {}", data); - Payload responsePayload = DefaultPayload.create("Echo: " + data); - p.release(); + Payload responsePayload = DefaultPayload.create("Echo: " + data); + p.release(); - return Mono.just(responsePayload); - })) - .bind(QuicTransportFactory.server("localhost", 7000)) - .delaySubscription(Duration.ofSeconds(5)) - .doOnNext(cc -> logger.info("Server started on the address : {}", cc.address())) - .block(); + return Mono.just(responsePayload); + })) + .bind(QuicTransportFactory.server("localhost", 7000)) + .delaySubscription(Duration.ofSeconds(5)) + .doOnNext(cc -> logger.info("Server started on the address : {}", cc.address())) + .block(); Mono source = RSocketConnector.create() .reconnect(Retry.backoff(50, Duration.ofMillis(500))) .connect(QuicTransportFactory.client("localhost", 7000)); - RSocketClient.from(source) - .requestResponse(Mono.just(DefaultPayload.create("Test Request"))) - .doOnSubscribe(s -> logger.info("Executing Request")) - .doOnNext( - d -> { - logger.info("Received response data {}", d.getDataUtf8()); - d.release(); - }) - .repeat(10) - .blockLast(); + RSocketClient client = RSocketClient.from(source); + + try { + client + .requestResponse(Mono.just(DefaultPayload.create("Test Request"))) + .doOnSubscribe(s -> logger.info("Executing Request")) + .doOnNext( + d -> { + logger.info("Received response data {}", d.getDataUtf8()); + d.release(); + }) + .repeat(10) + .blockLast(); + } finally { + ExampleLifecycle.close(client); + ExampleLifecycle.close(server); + } } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/client/RSocketClientExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/client/RSocketClientExample.java index dfbbcde53..379801838 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/client/RSocketClientExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/client/RSocketClientExample.java @@ -6,7 +6,9 @@ import io.rsocket.core.RSocketClient; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; +import io.rsocket.examples.transport.support.ExampleLifecycle; import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; import java.time.Duration; @@ -20,36 +22,44 @@ public class RSocketClientExample { public static void main(String[] args) { - RSocketServer.create( - SocketAcceptor.forRequestResponse( - p -> { - String data = p.getDataUtf8(); - logger.info("Received request data {}", data); + CloseableChannel server = + RSocketServer.create( + SocketAcceptor.forRequestResponse( + p -> { + String data = p.getDataUtf8(); + logger.info("Received request data {}", data); - Payload responsePayload = DefaultPayload.create("Echo: " + data); - p.release(); + Payload responsePayload = DefaultPayload.create("Echo: " + data); + p.release(); - return Mono.just(responsePayload); - })) - .bind(TcpServerTransport.create("localhost", 7000)) - .delaySubscription(Duration.ofSeconds(5)) - .doOnNext(cc -> logger.info("Server started on the address : {}", cc.address())) - .block(); + return Mono.just(responsePayload); + })) + .bind(TcpServerTransport.create("localhost", 7000)) + .delaySubscription(Duration.ofSeconds(5)) + .doOnNext(cc -> logger.info("Server started on the address : {}", cc.address())) + .block(); Mono source = RSocketConnector.create() .reconnect(Retry.backoff(50, Duration.ofMillis(500))) .connect(TcpClientTransport.create("localhost", 7000)); - RSocketClient.from(source) - .requestResponse(Mono.just(DefaultPayload.create("Test Request"))) - .doOnSubscribe(s -> logger.info("Executing Request")) - .doOnNext( - d -> { - logger.info("Received response data {}", d.getDataUtf8()); - d.release(); - }) - .repeat(10) - .blockLast(); + RSocketClient client = RSocketClient.from(source); + + try { + client + .requestResponse(Mono.just(DefaultPayload.create("Test Request"))) + .doOnSubscribe(s -> logger.info("Executing Request")) + .doOnNext( + d -> { + logger.info("Received response data {}", d.getDataUtf8()); + d.release(); + }) + .repeat(10) + .blockLast(); + } finally { + ExampleLifecycle.close(client); + ExampleLifecycle.close(server); + } } }