From 05a99a1c96c7a0ab5fb100cbf924efc7d808ce69 Mon Sep 17 00:00:00 2001 From: sullis Date: Wed, 27 Feb 2019 00:35:04 -0800 Subject: [PATCH 001/181] add [openjdk11] to Travis build matrix (#586) Signed-off-by: Sean Sullivan --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 2743ae0bc..3871823c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ language: java jdk: - oraclejdk8 -# - oraclejdk9 +- openjdk11 env: global: From b15a1fccae8a482dbf1a7e0d5cbdb538785b7e5e Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Wed, 27 Feb 2019 11:12:41 -0800 Subject: [PATCH 002/181] add fragmentation support using new frames (#585) * add fragmentation support using new frames Signed-off-by: Robert Roeser --- gradle.properties | 2 +- .../io/rsocket/ConnectionSetupPayload.java | 10 + .../src/main/java/io/rsocket/Payload.java | 20 +- .../main/java/io/rsocket/RSocketClient.java | 8 +- .../main/java/io/rsocket/RSocketFactory.java | 14 +- .../main/java/io/rsocket/RSocketServer.java | 43 +- .../FragmentationDuplexConnection.java | 170 +++--- .../fragmentation/FrameFragmenter.java | 334 +++++++----- .../fragmentation/FrameReassembler.java | 321 ++++++++---- .../frame/DataAndMetadataFlyweight.java | 9 +- .../rsocket/frame/FragmentationFlyweight.java | 21 + .../rsocket/frame/FrameHeaderFlyweight.java | 8 +- .../main/java/io/rsocket/frame/FrameUtil.java | 108 ++++ .../rsocket/frame/PayloadFrameFlyweight.java | 12 +- .../frame/RequestChannelFrameFlyweight.java | 22 + .../RequestFireAndForgetFrameFlyweight.java | 8 + .../frame/RequestResponseFrameFlyweight.java | 3 +- .../frame/RequestStreamFrameFlyweight.java | 14 +- .../ClientServerInputMultiplexer.java | 7 +- .../io/rsocket/transport/ClientTransport.java | 3 +- .../io/rsocket/transport/ServerTransport.java | 3 +- .../io/rsocket/uri/UriTransportRegistry.java | 12 +- .../java/io/rsocket/util/ByteBufPayload.java | 122 +++-- .../java/io/rsocket/util/DefaultPayload.java | 130 ++--- .../java/io/rsocket/util/EmptyPayload.java | 10 + .../java/io/rsocket/SetupRejectionTest.java | 2 +- .../FragmentationDuplexConnectionTest.java | 490 ++++++++---------- .../FragmentationIntegrationTest.java | 55 ++ .../fragmentation/FrameFragmenterTest.java | 421 ++++++++++----- .../fragmentation/FrameReassemblerTest.java | 478 ++++++++++++++--- .../java/io/rsocket/uri/TestUriHandler.java | 2 +- .../rsocket/uri/UriTransportRegistryTest.java | 4 +- .../transport/local/LocalClientTransport.java | 18 +- .../transport/local/LocalServerTransport.java | 45 +- .../local/LocalClientTransportTest.java | 6 +- .../local/LocalServerTransportTest.java | 6 +- .../transport/netty/SendPublisher.java | 29 +- .../transport/netty/TcpDuplexConnection.java | 52 +- .../netty/client/TcpClientTransport.java | 14 +- .../client/WebsocketClientTransport.java | 31 +- .../netty/server/TcpServerTransport.java | 14 +- .../netty/server/WebsocketRouteTransport.java | 39 +- .../server/WebsocketServerTransport.java | 18 +- .../netty/client/TcpClientTransportTest.java | 6 +- .../client/WebsocketClientTransportTest.java | 6 +- .../netty/server/TcpServerTransportTest.java | 4 +- .../server/WebsocketRouteTransportTest.java | 19 +- .../server/WebsocketServerTransportTest.java | 4 +- .../src/test/resources/logback-test.xml | 1 + 49 files changed, 2067 insertions(+), 1111 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java create mode 100644 rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java diff --git a/gradle.properties b/gradle.properties index 27abb8cac..9cd597b08 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,4 +12,4 @@ # limitations under the License. # -version=0.11.16-SNAPSHOT +version=0.12.1-SNAPSHOT diff --git a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java index 9f1d7ea6b..dcae3b74e 100644 --- a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java @@ -127,5 +127,15 @@ public ByteBuf sliceMetadata() { public ByteBuf sliceData() { return SetupFrameFlyweight.data(setupFrame); } + + @Override + public ByteBuf data() { + return sliceData(); + } + + @Override + public ByteBuf metadata() { + return sliceMetadata(); + } } } diff --git a/rsocket-core/src/main/java/io/rsocket/Payload.java b/rsocket-core/src/main/java/io/rsocket/Payload.java index 58fab3382..fc130528e 100644 --- a/rsocket-core/src/main/java/io/rsocket/Payload.java +++ b/rsocket-core/src/main/java/io/rsocket/Payload.java @@ -32,8 +32,8 @@ public interface Payload extends ReferenceCounted { boolean hasMetadata(); /** - * Returns the Payload metadata. Always non-null, check {@link #hasMetadata()} to differentiate - * null from "". + * Returns a slice Payload metadata. Always non-null, check {@link #hasMetadata()} to + * differentiate null from "". * * @return payload metadata. */ @@ -46,6 +46,22 @@ public interface Payload extends ReferenceCounted { */ ByteBuf sliceData(); + /** + * Returns the Payloads' data without slicing if possible. This is not safe and editing this could + * effect the payload. It is recommended to call sliceData(). + * + * @return data as a bytebuf or slice of the data + */ + ByteBuf data(); + + /** + * Returns the Payloads' metadata without slicing if possible. This is not safe and editing this + * could effect the payload. It is recommended to call sliceMetadata(). + * + * @return metadata as a bytebuf or slice of the metadata + */ + ByteBuf metadata(); + /** Increases the reference count by {@code 1}. */ @Override Payload retain(); diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java index 27a882d01..902d8487e 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java @@ -18,6 +18,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.ReferenceCountUtil; import io.netty.util.collection.IntObjectHashMap; import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.exceptions.Exceptions; @@ -220,7 +221,6 @@ private Mono handleFireAndForget(Payload payload) { false, payload.hasMetadata() ? payload.sliceMetadata().retain() : null, payload.sliceData().retain()); - payload.release(); sendProcessor.onNext(requestFrame); })); @@ -292,12 +292,12 @@ private Mono handleRequestResponse(final Payload payload) { false, payload.sliceMetadata().retain(), payload.sliceData().retain()); + payload.release(); UnicastMonoProcessor receiver = UnicastMonoProcessor.create(); receivers.put(streamId, receiver); sendProcessor.onNext(requestFrame); - return receiver .doOnError( t -> @@ -472,8 +472,10 @@ private void handleIncomingFrames(ByteBuf frame) { } else { handleFrame(streamId, type, frame); } - } finally { frame.release(); + } catch (Throwable t) { + ReferenceCountUtil.safeRelease(frame); + throw reactor.core.Exceptions.propagate(t); } } diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 8e8afda0a..1e7b056ca 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -20,7 +20,6 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.exceptions.InvalidSetupException; import io.rsocket.exceptions.RejectedSetupException; -import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.frame.ErrorFrameFlyweight; import io.rsocket.frame.SetupFrameFlyweight; import io.rsocket.frame.VersionFlyweight; @@ -216,7 +215,7 @@ private class StartClient implements Start { public Mono start() { return transportClient .get() - .connect() + .connect(mtu) .flatMap( connection -> { ByteBuf setupFrame = @@ -231,10 +230,6 @@ public Mono start() { setupPayload.sliceMetadata(), setupPayload.sliceData()); - if (mtu > 0) { - connection = new FragmentationDuplexConnection(connection, mtu); - } - ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(connection, plugins); @@ -333,10 +328,6 @@ public Mono start() { .get() .start( connection -> { - if (mtu > 0) { - connection = new FragmentationDuplexConnection(connection, mtu); - } - ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(connection, plugins); @@ -345,7 +336,8 @@ public Mono start() { .receive() .next() .flatMap(setupFrame -> processSetupFrame(multiplexer, setupFrame)); - }); + }, + mtu); } private Mono processSetupFrame( diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java index 2b0eadaf2..a184e11a1 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java @@ -34,6 +34,7 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.Disposable; +import reactor.core.Exceptions; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.SignalType; @@ -330,8 +331,10 @@ private void handleFrame(ByteBuf frame) { new IllegalStateException("ServerRSocket: Unexpected frame type: " + frameType)); break; } - } finally { ReferenceCountUtil.safeRelease(frame); + } catch (Throwable t) { + ReferenceCountUtil.safeRelease(frame); + throw Exceptions.propagate(t); } } @@ -345,11 +348,28 @@ private void handleFireAndForget(int streamId, Mono result) { private void handleRequestResponse(int streamId, Mono response) { response .doOnSubscribe(subscription -> sendingSubscriptions.put(streamId, subscription)) - .map(payload -> PayloadFrameFlyweight.encodeNextComplete(allocator, streamId, payload)) + .map( + payload -> { + ByteBuf byteBuf = null; + try { + byteBuf = PayloadFrameFlyweight.encodeNextComplete(allocator, streamId, payload); + } catch (Throwable t) { + if (byteBuf != null) { + ReferenceCountUtil.safeRelease(byteBuf); + ReferenceCountUtil.safeRelease(payload); + } + } + payload.release(); + return byteBuf; + }) .switchIfEmpty( Mono.fromCallable(() -> PayloadFrameFlyweight.encodeComplete(allocator, streamId))) .doFinally(signalType -> sendingSubscriptions.remove(streamId)) - .subscribe(t1 -> sendProcessor.onNext(t1), t -> handleError(streamId, t)); + .subscribe( + t1 -> { + sendProcessor.onNext(t1); + }, + t -> handleError(streamId, t)); } private void handleStream(int streamId, Flux response, int initialRequestN) { @@ -364,9 +384,20 @@ private void handleStream(int streamId, Flux response, int initialReque }) .doFinally(signalType -> sendingSubscriptions.remove(streamId)) .subscribe( - payload -> - sendProcessor.onNext( - PayloadFrameFlyweight.encodeNext(allocator, streamId, payload)), + payload -> { + ByteBuf byteBuf = null; + try { + byteBuf = PayloadFrameFlyweight.encodeNext(allocator, streamId, payload); + } catch (Throwable t) { + if (byteBuf != null) { + ReferenceCountUtil.safeRelease(byteBuf); + ReferenceCountUtil.safeRelease(payload); + } + throw Exceptions.propagate(t); + } + payload.release(); + sendProcessor.onNext(byteBuf); + }, t -> handleError(streamId, t), () -> sendProcessor.onNext(PayloadFrameFlyweight.encodeComplete(allocator, streamId))); } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java index af492508d..023c4e689 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java @@ -16,9 +16,18 @@ package io.rsocket.fragmentation; +import static io.rsocket.fragmentation.FrameFragmenter.fragmentFrame; + import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.frame.FrameType; +import java.util.Objects; import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -30,139 +39,84 @@ * and Reassembly */ public final class FragmentationDuplexConnection implements DuplexConnection { - public FragmentationDuplexConnection(DuplexConnection connection, int mtu) {} - - @Override - public Mono send(Publisher frames) { - return null; - } - - @Override - public Flux receive() { - return null; - } - - @Override - public Mono onClose() { - return null; - } - - @Override - public void dispose() {} - - /* - private final ByteBufAllocator byteBufAllocator; - + private static final int MIN_MTU_SIZE = 64; + private static final Logger logger = LoggerFactory.getLogger(FragmentationDuplexConnection.class); private final DuplexConnection delegate; + private final int mtu; + private final ByteBufAllocator allocator; + private final FrameReassembler frameReassembler; + private final boolean encodeLength; - private final FrameFragmenter frameFragmenter; - - private final IntObjectHashMap frameReassemblers = new IntObjectHashMap<>(); - - */ - /** - * Creates a new instance. - * - * @param delegate the {@link DuplexConnection} to decorate - * @param maxFragmentSize the maximum fragment size - * @throws NullPointerException if {@code delegate} is {@code null} - * @throws IllegalArgumentException if {@code maxFragmentSize} is not {@code positive} - */ - /* - // TODO: Remove once ByteBufAllocators are shared - public FragmentationDuplexConnection(DuplexConnection delegate, int maxFragmentSize) { - this(PooledByteBufAllocator.DEFAULT, delegate, maxFragmentSize); - } - - */ - /** - * Creates a new instance. - * - * @param byteBufAllocator the {@link ByteBufAllocator} to use - * @param delegate the {@link DuplexConnection} to decorate - * @param maxFragmentSize the maximum fragment size. A value of 0 indicates that frames should not - * be fragmented. - * @throws NullPointerException if {@code byteBufAllocator} or {@code delegate} are {@code null} - * @throws IllegalArgumentException if {@code maxFragmentSize} is not {@code positive} - */ - /* public FragmentationDuplexConnection( - ByteBufAllocator byteBufAllocator, DuplexConnection delegate, int maxFragmentSize) { - - this.byteBufAllocator = - Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); - this.delegate = Objects.requireNonNull(delegate, "delegate must not be null"); - - NumberUtils.requireNonNegative(maxFragmentSize, "maxFragmentSize must be positive"); + DuplexConnection delegate, ByteBufAllocator allocator, int mtu, boolean encodeLength) { + Objects.requireNonNull(delegate, "delegate must not be null"); + Objects.requireNonNull(allocator, "byteBufAllocator must not be null"); + if (mtu < MIN_MTU_SIZE) { + throw new IllegalArgumentException("smallest allowed mtu size is " + MIN_MTU_SIZE + " bytes"); + } + this.encodeLength = encodeLength; + this.allocator = allocator; + this.delegate = delegate; + this.mtu = mtu; + this.frameReassembler = new FrameReassembler(allocator); - this.frameFragmenter = new FrameFragmenter(byteBufAllocator, maxFragmentSize); + delegate.onClose().doFinally(s -> frameReassembler.dispose()).subscribe(); + } - delegate - .onClose() - .doFinally( - signalType -> { - Collection values; - synchronized (FragmentationDuplexConnection.this) { - values = frameReassemblers.values(); - } - values.forEach(FrameReassembler::dispose); - }) - .subscribe(); + private boolean shouldFragment(FrameType frameType, int readableBytes) { + return frameType.isFragmentable() && readableBytes > mtu; } @Override - public double availability() { - return delegate.availability(); + public Mono send(Publisher frames) { + return Flux.from(frames).concatMap(this::sendOne).then(); } @Override - public void dispose() { - delegate.dispose(); + public Mono sendOne(ByteBuf frame) { + FrameType frameType = FrameHeaderFlyweight.frameType(frame); + int readableBytes = frame.readableBytes(); + if (shouldFragment(frameType, readableBytes)) { + return delegate.send(fragmentFrame(allocator, mtu, frame, frameType, encodeLength)); + } else { + return delegate.sendOne(encode(frame)); + } } - @Override - public boolean isDisposed() { - return delegate.isDisposed(); + private ByteBuf encode(ByteBuf frame) { + if (encodeLength) { + return FrameLengthFlyweight.encode(allocator, frame.readableBytes(), frame).retain(); + } else { + return frame; + } } - @Override - public Mono onClose() { - return delegate.onClose(); + private ByteBuf decode(ByteBuf frame) { + if (encodeLength) { + return FrameLengthFlyweight.frame(frame).retain(); + } else { + return frame; + } } @Override public Flux receive() { return delegate .receive() - .map(AbstractionLeakingFrameUtils::fromAbstractionLeakingFrame) - .concatMap(t2 -> toReassembledFrames(t2.getT1(), t2.getT2())); + .handle( + (byteBuf, sink) -> { + ByteBuf decode = decode(byteBuf); + frameReassembler.reassembleFrame(decode, sink); + }); } @Override - public Mono send(Publisher frames) { - Objects.requireNonNull(frames, "frames must not be null"); - - return delegate.send( - Flux.from(frames) - .map(AbstractionLeakingFrameUtils::fromAbstractionLeakingFrame) - .concatMap(t2 -> toFragmentedFrames(t2.getT1(), t2.getT2()))); + public Mono onClose() { + return delegate.onClose(); } - private Flux toFragmentedFrames(int streamId, io.rsocket.framing.Frame frame) { - return this.frameFragmenter - .fragment(frame) - .map(fragment -> toAbstractionLeakingFrame(byteBufAllocator, streamId, fragment)); + @Override + public void dispose() { + delegate.dispose(); } - - private Mono toReassembledFrames(int streamId, io.rsocket.framing.Frame fragment) { - FrameReassembler frameReassembler; - synchronized (this) { - frameReassembler = - frameReassemblers.computeIfAbsent( - streamId, i -> createFrameReassembler(byteBufAllocator)); - } - - return Mono.justOrEmpty(frameReassembler.reassemble(fragment)) - .map(frame -> toAbstractionLeakingFrame(byteBufAllocator, streamId, frame)); - }*/ } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java index e9b4de243..d634f7374 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java @@ -16,6 +16,16 @@ package io.rsocket.fragmentation; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCountUtil; +import io.rsocket.frame.*; +import java.util.function.Consumer; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.SynchronousSink; + /** * The implementation of the RSocket fragmentation behavior. * @@ -24,168 +34,208 @@ * and Reassembly */ final class FrameFragmenter { - /* - private final ByteBufAllocator byteBufAllocator; - - private final Logger logger = LoggerFactory.getLogger(this.getClass()); - - private final int maxFragmentSize; - - */ - /** - * Creates a new instance - * - * @param byteBufAllocator the {@link ByteBufAllocator} to use - * @param maxFragmentSize the maximum size of each fragment - */ - /* - FrameFragmenter(ByteBufAllocator byteBufAllocator, int maxFragmentSize) { - this.byteBufAllocator = - Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); - this.maxFragmentSize = maxFragmentSize; - } - - */ - /** - * Returns a {@link Flux} of fragments frames - * - * @param frame the {@link ByteBuf} to fragment - * @return a {@link Flux} of fragment frames - * @throws NullPointerException if {@code frame} is {@code null} - */ - /* - public Flux fragment(ByteBuf frame) { - Objects.requireNonNull(frame, "frame must not be null"); - - if (!shouldFragment(frame)) { - logger.debug("Not fragmenting {}", frame); - return Flux.just(frame); - } - - logger.debug("Fragmenting {}", frame); + static Publisher fragmentFrame( + ByteBufAllocator allocator, + int mtu, + final ByteBuf frame, + FrameType frameType, + boolean encodeLength) { + ByteBuf metadata = getMetadata(frame, frameType); + ByteBuf data = getData(frame, frameType); + int streamId = FrameHeaderFlyweight.streamId(frame); return Flux.generate( - () -> new FragmentationState((FragmentableFrame) frame), - this::generate, - FragmentationState::dispose); + new Consumer>() { + boolean first = true; + + @Override + public void accept(SynchronousSink sink) { + ByteBuf byteBuf; + if (first) { + first = false; + byteBuf = + encodeFirstFragment( + allocator, mtu, frame, frameType, streamId, metadata, data); + } else { + byteBuf = encodeFollowsFragment(allocator, mtu, streamId, metadata, data); + } + + sink.next(encode(allocator, byteBuf, encodeLength)); + if (!metadata.isReadable() && !data.isReadable()) { + sink.complete(); + } + } + }) + .doFinally(signalType -> ReferenceCountUtil.safeRelease(frame)); } - private FragmentationState generate(FragmentationState state, SynchronousSink sink) { - int fragmentLength = maxFragmentSize; - - ByteBuf metadata; - if (state.hasReadableMetadata()) { - metadata = state.readMetadataFragment(fragmentLength); - fragmentLength -= metadata.readableBytes(); - } else { - metadata = null; + static ByteBuf encodeFirstFragment( + ByteBufAllocator allocator, + int mtu, + ByteBuf frame, + FrameType frameType, + int streamId, + ByteBuf metadata, + ByteBuf data) { + // subtract the header bytes + int remaining = mtu - FrameHeaderFlyweight.size(); + + // substract the initial request n + switch (frameType) { + case REQUEST_STREAM: + case REQUEST_CHANNEL: + remaining -= Integer.BYTES; + break; + default: } - if (state.hasReadableMetadata()) { - ByteBuf fragment = state.createFrame(byteBufAllocator, false, metadata, null); - logger.debug("Fragment {}", fragment); - - sink.next(fragment); - return state; + ByteBuf metadataFragment = null; + if (metadata.isReadable()) { + // subtract the metadata frame length + remaining -= 3; + int r = Math.min(remaining, metadata.readableBytes()); + remaining -= r; + metadataFragment = metadata.readRetainedSlice(r); } - ByteBuf data; - data = state.hasReadableData() ? state.readDataFragment(fragmentLength) : null; - - if (state.hasReadableData()) { - ByteBuf fragment = state.createFrame(byteBufAllocator, false, metadata, data); - logger.debug("Fragment {}", fragment); - - sink.next(fragment); - return state; + ByteBuf dataFragment = Unpooled.EMPTY_BUFFER; + if (remaining > 0 && data.isReadable()) { + int r = Math.min(remaining, data.readableBytes()); + dataFragment = data.readRetainedSlice(r); } - ByteBuf fragment = state.createFrame(byteBufAllocator, true, metadata, data); - logger.debug("Final Fragment {}", fragment); - - sink.next(fragment); - sink.complete(); - return state; - } - - private int getFragmentableLength(FragmentableFrame fragmentableFrame) { - return fragmentableFrame.getMetadataLength().orElse(0) + fragmentableFrame.getDataLength(); - } - - private boolean shouldFragment(ByteBuf frame) { - if (maxFragmentSize == 0 || !(frame instanceof FragmentableFrame)) { - return false; + switch (frameType) { + case REQUEST_FNF: + return RequestFireAndForgetFrameFlyweight.encode( + allocator, streamId, true, metadataFragment, dataFragment); + case REQUEST_STREAM: + return RequestStreamFrameFlyweight.encode( + allocator, + streamId, + true, + RequestStreamFrameFlyweight.initialRequestN(frame), + metadataFragment, + dataFragment); + case REQUEST_RESPONSE: + return RequestResponseFrameFlyweight.encode( + allocator, streamId, true, metadataFragment, dataFragment); + case REQUEST_CHANNEL: + return RequestChannelFrameFlyweight.encode( + allocator, + streamId, + true, + false, + RequestChannelFrameFlyweight.initialRequestN(frame), + metadataFragment, + dataFragment); + // Payload and synthetic types + case PAYLOAD: + return PayloadFrameFlyweight.encode( + allocator, streamId, true, false, false, metadataFragment, dataFragment); + case NEXT: + return PayloadFrameFlyweight.encode( + allocator, streamId, true, false, true, metadataFragment, dataFragment); + case NEXT_COMPLETE: + return PayloadFrameFlyweight.encode( + allocator, streamId, true, true, true, metadataFragment, dataFragment); + case COMPLETE: + return PayloadFrameFlyweight.encode( + allocator, streamId, true, true, false, metadataFragment, dataFragment); + default: + throw new IllegalStateException("unsupported fragment type: " + frameType); } - - FragmentableFrame fragmentableFrame = (FragmentableFrame) frame; - return !fragmentableFrame.isFollowsFlagSet() - && getFragmentableLength(fragmentableFrame) > maxFragmentSize; } - static final class FragmentationState implements Disposable { - - private final FragmentableFrame frame; - - private int dataIndex = 0; - - private boolean initialFragmentCreated = false; - - private int metadataIndex = 0; - - FragmentationState(FragmentableFrame frame) { - this.frame = frame; + static ByteBuf encodeFollowsFragment( + ByteBufAllocator allocator, int mtu, int streamId, ByteBuf metadata, ByteBuf data) { + // subtract the header bytes + int remaining = mtu - FrameHeaderFlyweight.size(); + + ByteBuf metadataFragment = null; + if (metadata.isReadable()) { + // subtract the metadata frame length + remaining -= 3; + int r = Math.min(remaining, metadata.readableBytes()); + remaining -= r; + metadataFragment = metadata.readRetainedSlice(r); } - @Override - public void dispose() { - disposeQuietly(frame); + ByteBuf dataFragment = Unpooled.EMPTY_BUFFER; + if (remaining > 0 && data.isReadable()) { + int r = Math.min(remaining, data.readableBytes()); + dataFragment = data.readRetainedSlice(r); } - ByteBuf createFrame( - ByteBufAllocator byteBufAllocator, - boolean complete, - @Nullable ByteBuf metadata, - @Nullable ByteBuf data) { - - if (initialFragmentCreated) { - return createPayloadFrame(byteBufAllocator, !complete, data == null, metadata, data); - } else { - initialFragmentCreated = true; - return frame.createFragment(byteBufAllocator, metadata, data); - } - } - - boolean hasReadableData() { - return frame.getDataLength() - dataIndex > 0; - } + boolean follows = data.isReadable() || metadata.isReadable(); + return PayloadFrameFlyweight.encode( + allocator, streamId, follows, false, true, metadataFragment, dataFragment); + } - boolean hasReadableMetadata() { - Integer metadataLength = frame.getUnsafeMetadataLength(); - return metadataLength != null && metadataLength - metadataIndex > 0; + static ByteBuf getMetadata(ByteBuf frame, FrameType frameType) { + boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(frame); + if (hasMetadata) { + ByteBuf metadata; + switch (frameType) { + case REQUEST_FNF: + metadata = RequestFireAndForgetFrameFlyweight.metadata(frame); + break; + case REQUEST_STREAM: + metadata = RequestStreamFrameFlyweight.metadata(frame); + break; + case REQUEST_RESPONSE: + metadata = RequestResponseFrameFlyweight.metadata(frame); + break; + case REQUEST_CHANNEL: + metadata = RequestChannelFrameFlyweight.metadata(frame); + break; + // Payload and synthetic types + case PAYLOAD: + case NEXT: + case NEXT_COMPLETE: + case COMPLETE: + metadata = PayloadFrameFlyweight.metadata(frame); + break; + default: + throw new IllegalStateException("unsupported fragment type"); + } + return metadata; + } else { + return Unpooled.EMPTY_BUFFER; } + } - ByteBuf readDataFragment(int length) { - int safeLength = min(length, frame.getDataLength() - dataIndex); - - ByteBuf fragment = frame.getUnsafeData().slice(dataIndex, safeLength); - - dataIndex += fragment.readableBytes(); - return fragment; + static ByteBuf getData(ByteBuf frame, FrameType frameType) { + ByteBuf data; + switch (frameType) { + case REQUEST_FNF: + data = RequestFireAndForgetFrameFlyweight.data(frame); + break; + case REQUEST_STREAM: + data = RequestStreamFrameFlyweight.data(frame); + break; + case REQUEST_RESPONSE: + data = RequestResponseFrameFlyweight.data(frame); + break; + case REQUEST_CHANNEL: + data = RequestChannelFrameFlyweight.data(frame); + break; + // Payload and synthetic types + case PAYLOAD: + case NEXT: + case NEXT_COMPLETE: + case COMPLETE: + data = PayloadFrameFlyweight.data(frame); + break; + default: + throw new IllegalStateException("unsupported fragment type"); } + return data; + } - ByteBuf readMetadataFragment(int length) { - Integer metadataLength = frame.getUnsafeMetadataLength(); - ByteBuf metadata = frame.getUnsafeMetadata(); - - if (metadataLength == null || metadata == null) { - throw new IllegalStateException("Cannot read metadata fragment with no metadata"); - } - - int safeLength = min(length, metadataLength - metadataIndex); - - ByteBuf fragment = metadata.slice(metadataIndex, safeLength); - - metadataIndex += fragment.readableBytes(); - return fragment; + static ByteBuf encode(ByteBufAllocator allocator, ByteBuf frame, boolean encodeLength) { + if (encodeLength) { + return FrameLengthFlyweight.encode(allocator, frame.readableBytes(), frame); + } else { + return frame; } - }*/ + } } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java index a44883915..1d0ae6792 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java @@ -16,7 +16,19 @@ package io.rsocket.fragmentation; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; +import io.rsocket.frame.*; +import java.util.concurrent.atomic.AtomicBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.Disposable; +import reactor.core.publisher.SynchronousSink; /** * The implementation of the RSocket reassembly behavior. @@ -25,143 +37,252 @@ * href="https://github.com/rsocket/rsocket/blob/master/Protocol.md#fragmentation-and-reassembly">Fragmentation * and Reassembly */ -final class FrameReassembler implements Disposable { +final class FrameReassembler extends AtomicBoolean implements Disposable { + private static final Logger logger = LoggerFactory.getLogger(FrameReassembler.class); + + final IntObjectMap headers; + final IntObjectMap metadata; + final IntObjectMap data; + + private final ByteBufAllocator allocator; + + public FrameReassembler(ByteBufAllocator allocator) { + this.allocator = allocator; + this.headers = new IntObjectHashMap<>(); + this.metadata = new IntObjectHashMap<>(); + this.data = new IntObjectHashMap<>(); + } + @Override - public void dispose() {} + public void dispose() { + if (compareAndSet(false, true)) { + synchronized (FrameReassembler.this) { + for (ByteBuf byteBuf : headers.values()) { + ReferenceCountUtil.safeRelease(byteBuf); + } + headers.clear(); + + for (ByteBuf byteBuf : metadata.values()) { + ReferenceCountUtil.safeRelease(byteBuf); + } + metadata.clear(); + + for (ByteBuf byteBuf : data.values()) { + ReferenceCountUtil.safeRelease(byteBuf); + } + data.clear(); + } + } + } @Override public boolean isDisposed() { - return false; + return get(); } - /* - private static final Recycler RECYCLER = createRecycler(FrameReassembler::new); - private final Handle handle; + synchronized ByteBuf getHeader(int streamId) { + return headers.get(streamId); + } - private ByteBufAllocator byteBufAllocator; + synchronized CompositeByteBuf getMetadata(int streamId) { + CompositeByteBuf byteBuf = metadata.get(streamId); - private ReassemblyState state; + if (byteBuf == null) { + byteBuf = allocator.compositeBuffer(); + metadata.put(streamId, byteBuf); + } - private FrameReassembler(Handle handle) { - this.handle = handle; + return byteBuf; } - @Override - public void dispose() { - if (state != null) { - disposeQuietly(state); - } + synchronized CompositeByteBuf getData(int streamId) { + CompositeByteBuf byteBuf = data.get(streamId); - byteBufAllocator = null; - state = null; - - handle.recycle(this); - } - - */ - /** - * Creates a new instance - * - * @param byteBufAllocator the {@link ByteBufAllocator} to use - * @return the {@code FrameReassembler} - * @throws NullPointerException if {@code byteBufAllocator} is {@code null} - */ - /* - static FrameReassembler createFrameReassembler(ByteBufAllocator byteBufAllocator) { - return RECYCLER.get().setByteBufAllocator(byteBufAllocator); - } - - */ - /** - * Reassembles a frame. If the frame is not a candidate for fragmentation, emits the frame. If - * frame is a candidate for fragmentation, accumulates the content until the final fragment. - * - * @param frame the frame to inspect for reassembly - * @return the reassembled frame if complete, otherwise {@code null} - * @throws NullPointerException if {@code frame} is {@code null} - */ - /* - @Nullable - Frame reassemble(Frame frame) { - Objects.requireNonNull(frame, "frame must not be null"); - - if (!(frame instanceof FragmentableFrame)) { - return frame; + if (byteBuf == null) { + byteBuf = allocator.compositeBuffer(); + data.put(streamId, byteBuf); } - FragmentableFrame fragmentableFrame = (FragmentableFrame) frame; - - if (fragmentableFrame.isFollowsFlagSet()) { - if (state == null) { - state = new ReassemblyState(fragmentableFrame); - } else { - state.accumulate(fragmentableFrame); - } - } else if (state != null) { - state.accumulate(fragmentableFrame); - - Frame reassembledFrame = state.createFrame(byteBufAllocator); - state.dispose(); - state = null; + return byteBuf; + } - return reassembledFrame; - } else { - return fragmentableFrame; - } + synchronized ByteBuf removeHeader(int streamId) { + return headers.remove(streamId); + } - return null; + synchronized CompositeByteBuf removeMetadata(int streamId) { + return metadata.remove(streamId); } - FrameReassembler setByteBufAllocator(ByteBufAllocator byteBufAllocator) { - this.byteBufAllocator = - Objects.requireNonNull(byteBufAllocator, "byteBufAllocator must not be null"); + synchronized CompositeByteBuf removeData(int streamId) { + return data.remove(streamId); + } - return this; + synchronized void putHeader(int streamId, ByteBuf header) { + headers.put(streamId, header); } - static final class ReassemblyState implements Disposable { + void cancelAssemble(int streamId) { + ByteBuf header = removeHeader(streamId); + CompositeByteBuf metadata = removeMetadata(streamId); + CompositeByteBuf data = removeData(streamId); - private ByteBuf data; + if (header != null) { + ReferenceCountUtil.safeRelease(header); + } - private List fragments = new ArrayList<>(); + if (metadata != null) { + ReferenceCountUtil.safeRelease(metadata); + } - private ByteBuf metadata; + if (data != null) { + ReferenceCountUtil.safeRelease(data); + } + } - ReassemblyState(FragmentableFrame fragment) { - accumulate(fragment); + void handleNoFollowsFlag(ByteBuf frame, SynchronousSink sink, int streamId) { + ByteBuf header = removeHeader(streamId); + if (header != null) { + if (FrameHeaderFlyweight.hasMetadata(header)) { + ByteBuf assembledFrame = assembleFrameWithMetadata(frame, streamId, header); + sink.next(assembledFrame); + } else { + ByteBuf data = assembleData(frame, streamId); + ByteBuf assembledFrame = FragmentationFlyweight.encode(allocator, header, data); + sink.next(assembledFrame); + } + } else { + sink.next(frame); } + } - @Override - public void dispose() { - fragments.forEach(Disposable::dispose); + void handleFollowsFlag(ByteBuf frame, int streamId, FrameType frameType) { + ByteBuf header = getHeader(streamId); + if (header == null) { + header = frame.copy(frame.readerIndex(), FrameHeaderFlyweight.size()); + + if (frameType == FrameType.REQUEST_CHANNEL || frameType == FrameType.REQUEST_STREAM) { + int i = RequestChannelFrameFlyweight.initialRequestN(frame); + header.writeInt(i); + } + putHeader(streamId, header); } - void accumulate(FragmentableFrame fragment) { - fragments.add(fragment); - metadata = accumulateMetadata(fragment); - data = accumulateData(fragment); + if (FrameHeaderFlyweight.hasMetadata(frame)) { + CompositeByteBuf metadata = getMetadata(streamId); + switch (frameType) { + case REQUEST_FNF: + metadata.addComponents(true, RequestFireAndForgetFrameFlyweight.metadata(frame)); + break; + case REQUEST_STREAM: + metadata.addComponents(true, RequestStreamFrameFlyweight.metadata(frame)); + break; + case REQUEST_RESPONSE: + metadata.addComponents(true, RequestResponseFrameFlyweight.metadata(frame)); + break; + case REQUEST_CHANNEL: + metadata.addComponents(true, RequestChannelFrameFlyweight.metadata(frame)); + break; + // Payload and synthetic types + case PAYLOAD: + case NEXT: + case NEXT_COMPLETE: + case COMPLETE: + metadata.addComponents(true, PayloadFrameFlyweight.metadata(frame)); + break; + default: + throw new IllegalStateException("unsupported fragment type"); + } } - Frame createFrame(ByteBufAllocator byteBufAllocator) { - FragmentableFrame root = fragments.get(0); - return root.createNonFragment(byteBufAllocator, metadata, data); + ByteBuf data; + switch (frameType) { + case REQUEST_FNF: + data = RequestFireAndForgetFrameFlyweight.data(frame); + break; + case REQUEST_STREAM: + data = RequestStreamFrameFlyweight.data(frame); + break; + case REQUEST_RESPONSE: + data = RequestResponseFrameFlyweight.data(frame); + break; + case REQUEST_CHANNEL: + data = RequestChannelFrameFlyweight.data(frame); + break; + // Payload and synthetic types + case PAYLOAD: + case NEXT: + case NEXT_COMPLETE: + case COMPLETE: + data = PayloadFrameFlyweight.data(frame); + break; + default: + throw new IllegalStateException("unsupported fragment type"); } - private ByteBuf accumulateData(FragmentableFrame fragment) { - ByteBuf data = fragment.getUnsafeData(); - return this.data == null ? data.retain() : Unpooled.wrappedBuffer(this.data, data.retain()); + if (data != Unpooled.EMPTY_BUFFER) { + getData(streamId).addComponents(true, data); } + } + + void reassembleFrame(ByteBuf frame, SynchronousSink sink) { + try { + FrameType frameType = FrameHeaderFlyweight.frameType(frame); + int streamId = FrameHeaderFlyweight.streamId(frame); + switch (frameType) { + case CANCEL: + case ERROR: + cancelAssemble(streamId); + default: + } - private @Nullable ByteBuf accumulateMetadata(FragmentableFrame fragment) { - ByteBuf metadata = fragment.getUnsafeMetadata(); + if (!frameType.isFragmentable()) { + sink.next(frame); + return; + } + + boolean hasFollows = FrameHeaderFlyweight.hasFollows(frame); - if (metadata == null) { - return this.metadata; + if (!hasFollows) { + handleNoFollowsFlag(frame, sink, streamId); + } else { + handleFollowsFlag(frame, streamId, frameType); } - return this.metadata == null - ? metadata.retain() - : Unpooled.wrappedBuffer(this.metadata, metadata.retain()); + } catch (Throwable t) { + logger.error("error reassemble frame", t); + sink.error(t); } - }*/ + } + + private ByteBuf assembleFrameWithMetadata(ByteBuf frame, int streamId, ByteBuf header) { + ByteBuf metadata; + CompositeByteBuf cm = removeMetadata(streamId); + if (cm != null) { + ByteBuf m = PayloadFrameFlyweight.metadata(frame); + metadata = cm.addComponents(true, m); + } else { + metadata = PayloadFrameFlyweight.metadata(frame); + } + + ByteBuf data = assembleData(frame, streamId); + + return FragmentationFlyweight.encode(allocator, header, metadata, data); + } + + private ByteBuf assembleData(ByteBuf frame, int streamId) { + ByteBuf data; + CompositeByteBuf cd = removeData(streamId); + if (cd != null) { + ByteBuf d = PayloadFrameFlyweight.data(frame); + if (d != null) { + cd.addComponents(true, d); + } + data = cd; + } else { + data = Unpooled.EMPTY_BUFFER; + } + + return data; + } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java index f07f5f004..6a493fffe 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java @@ -21,9 +21,12 @@ private static void encodeLength(final ByteBuf byteBuf, final int length) { } private static int decodeLength(final ByteBuf byteBuf) { - int length = (byteBuf.readByte() & 0xFF) << 16; - length |= (byteBuf.readByte() & 0xFF) << 8; - length |= byteBuf.readByte() & 0xFF; + byte b = byteBuf.readByte(); + int length = (b & 0xFF) << 16; + byte b1 = byteBuf.readByte(); + length |= (b1 & 0xFF) << 8; + byte b2 = byteBuf.readByte(); + length |= b2 & 0xFF; return length; } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java new file mode 100644 index 000000000..d5b3742b5 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java @@ -0,0 +1,21 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import reactor.util.annotation.Nullable; + +/** FragmentationFlyweight is used to re-assemble frames */ +public class FragmentationFlyweight { + public static ByteBuf encode(final ByteBufAllocator allocator, ByteBuf header, ByteBuf data) { + return encode(allocator, header, null, data); + } + + public static ByteBuf encode( + final ByteBufAllocator allocator, ByteBuf header, @Nullable ByteBuf metadata, ByteBuf data) { + if (metadata == null) { + return DataAndMetadataFlyweight.encodeOnlyData(allocator, header, data); + } else { + return DataAndMetadataFlyweight.encode(allocator, header, metadata, data); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java index 7dbe8053a..7f03984d8 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java @@ -45,7 +45,7 @@ static ByteBuf encodeStreamZero( return encode(allocator, 0, frameType, flags); } - static ByteBuf encode( + public static ByteBuf encode( final ByteBufAllocator allocator, final int streamId, final FrameType frameType, int flags) { if (!frameType.canHaveMetadata() && ((flags & FLAGS_M) == FLAGS_M)) { throw new IllegalStateException("bad value for metadata flag"); @@ -56,6 +56,10 @@ static ByteBuf encode( return allocator.buffer().writeInt(streamId).writeShort(typeAndFlags); } + public static boolean hasFollows(ByteBuf byteBuf) { + return (flags(byteBuf) & FLAGS_F) == FLAGS_F; + } + public static int streamId(ByteBuf byteBuf) { byteBuf.markReaderIndex(); int streamId = byteBuf.readInt(); @@ -113,7 +117,7 @@ public static void ensureFrameType(final FrameType frameType, ByteBuf byteBuf) { } } - static int size() { + public static int size() { return HEADER_SIZE; } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java new file mode 100644 index 000000000..f9ae72ed2 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java @@ -0,0 +1,108 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; + +public class FrameUtil { + private FrameUtil() {} + + public static String toString(ByteBuf frame) { + FrameType frameType = FrameHeaderFlyweight.frameType(frame); + int streamId = FrameHeaderFlyweight.streamId(frame); + StringBuilder payload = new StringBuilder(); + + payload + .append("\nFrame => Stream ID: ") + .append(streamId) + .append(" Type: ") + .append(frameType) + .append(" Flags: 0b") + .append(Integer.toBinaryString(FrameHeaderFlyweight.flags(frame))) + .append(" Length: " + frame.readableBytes()); + + if (FrameHeaderFlyweight.hasMetadata(frame)) { + payload.append("\nMetadata:\n"); + + ByteBufUtil.appendPrettyHexDump(payload, getMetadata(frame, frameType)); + } + + payload.append("\nData:\n"); + ByteBufUtil.appendPrettyHexDump(payload, getData(frame, frameType)); + + return payload.toString(); + } + + private static ByteBuf getMetadata(ByteBuf frame, FrameType frameType) { + boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(frame); + if (hasMetadata) { + ByteBuf metadata; + switch (frameType) { + case REQUEST_FNF: + metadata = RequestFireAndForgetFrameFlyweight.metadata(frame); + break; + case REQUEST_STREAM: + metadata = RequestStreamFrameFlyweight.metadata(frame); + break; + case REQUEST_RESPONSE: + metadata = RequestResponseFrameFlyweight.metadata(frame); + break; + case REQUEST_CHANNEL: + metadata = RequestChannelFrameFlyweight.metadata(frame); + break; + // Payload and synthetic types + case PAYLOAD: + case NEXT: + case NEXT_COMPLETE: + case COMPLETE: + metadata = PayloadFrameFlyweight.metadata(frame); + break; + case METADATA_PUSH: + metadata = MetadataPushFrameFlyweight.metadata(frame); + break; + case SETUP: + metadata = SetupFrameFlyweight.metadata(frame); + break; + case LEASE: + metadata = LeaseFlyweight.metadata(frame); + break; + default: + return Unpooled.EMPTY_BUFFER; + } + return metadata.retain(); + } else { + return Unpooled.EMPTY_BUFFER; + } + } + + private static ByteBuf getData(ByteBuf frame, FrameType frameType) { + ByteBuf data; + switch (frameType) { + case REQUEST_FNF: + data = RequestFireAndForgetFrameFlyweight.data(frame); + break; + case REQUEST_STREAM: + data = RequestStreamFrameFlyweight.data(frame); + break; + case REQUEST_RESPONSE: + data = RequestResponseFrameFlyweight.data(frame); + break; + case REQUEST_CHANNEL: + data = RequestChannelFrameFlyweight.data(frame); + break; + // Payload and synthetic types + case PAYLOAD: + case NEXT: + case NEXT_COMPLETE: + case COMPLETE: + data = PayloadFrameFlyweight.data(frame); + break; + case SETUP: + data = SetupFrameFlyweight.data(frame); + break; + default: + return Unpooled.EMPTY_BUFFER; + } + return data.retain(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java index 83f2406dd..4f67d9c72 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java @@ -35,8 +35,8 @@ public static ByteBuf encode( complete, next, 0, - payload.hasMetadata() ? payload.sliceMetadata().retain() : null, - payload.sliceData().retain()); + payload.hasMetadata() ? payload.metadata().retain() : null, + payload.data().retain()); } public static ByteBuf encodeNextComplete( @@ -48,8 +48,8 @@ public static ByteBuf encodeNextComplete( true, true, 0, - payload.hasMetadata() ? payload.sliceMetadata().retain() : null, - payload.sliceData().retain()); + payload.hasMetadata() ? payload.metadata().retain() : null, + payload.data().retain()); } public static ByteBuf encodeNext(ByteBufAllocator allocator, int streamId, Payload payload) { @@ -60,8 +60,8 @@ public static ByteBuf encodeNext(ByteBufAllocator allocator, int streamId, Paylo false, true, 0, - payload.hasMetadata() ? payload.sliceMetadata().retain() : null, - payload.sliceData().retain()); + payload.hasMetadata() ? payload.metadata().retain() : null, + payload.data().retain()); } public static ByteBuf encodeComplete(ByteBufAllocator allocator, int streamId) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java index fb6ecebb0..06ddcda03 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java @@ -2,6 +2,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; public class RequestChannelFrameFlyweight { @@ -9,6 +10,27 @@ public class RequestChannelFrameFlyweight { private RequestChannelFrameFlyweight() {} + public static ByteBuf encode( + ByteBufAllocator allocator, + int streamId, + boolean fragmentFollows, + boolean complete, + long requestN, + Payload payload) { + + int reqN = requestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) requestN; + + return FLYWEIGHT.encode( + allocator, + streamId, + fragmentFollows, + complete, + false, + reqN, + payload.metadata(), + payload.data()); + } + public static ByteBuf encode( ByteBufAllocator allocator, int streamId, diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java index 680374f71..5f2d606e4 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java @@ -2,6 +2,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; public class RequestFireAndForgetFrameFlyweight { @@ -9,6 +10,13 @@ public class RequestFireAndForgetFrameFlyweight { private RequestFireAndForgetFrameFlyweight() {} + public static ByteBuf encode( + ByteBufAllocator allocator, int streamId, boolean fragmentFollows, Payload payload) { + + return FLYWEIGHT.encode( + allocator, streamId, fragmentFollows, payload.metadata(), payload.data()); + } + public static ByteBuf encode( ByteBufAllocator allocator, int streamId, diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java index efbffbd40..2e06c9b82 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java @@ -12,8 +12,7 @@ private RequestResponseFrameFlyweight() {} public static ByteBuf encode( ByteBufAllocator allocator, int streamId, boolean fragmentFollows, Payload payload) { - return encode( - allocator, streamId, fragmentFollows, payload.sliceMetadata(), payload.sliceData()); + return encode(allocator, streamId, fragmentFollows, payload.metadata(), payload.data()); } public static ByteBuf encode( diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java index 3e858f5d4..171c41990 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java @@ -17,12 +17,7 @@ public static ByteBuf encode( long requestN, Payload payload) { return encode( - allocator, - streamId, - fragmentFollows, - requestN, - payload.sliceMetadata(), - payload.sliceData()); + allocator, streamId, fragmentFollows, requestN, payload.metadata(), payload.data()); } public static ByteBuf encode( @@ -32,12 +27,7 @@ public static ByteBuf encode( int requestN, Payload payload) { return encode( - allocator, - streamId, - fragmentFollows, - requestN, - payload.sliceMetadata(), - payload.sliceData()); + allocator, streamId, fragmentFollows, requestN, payload.metadata(), payload.data()); } public static ByteBuf encode( diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index e6178bd5b..5c0c1d74f 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -21,6 +21,7 @@ import io.rsocket.DuplexConnection; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; +import io.rsocket.frame.FrameUtil; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; import io.rsocket.plugins.PluginRegistry; import org.reactivestreams.Publisher; @@ -148,7 +149,7 @@ public InternalDuplexConnection( @Override public Mono send(Publisher frame) { if (debugEnabled) { - frame = Flux.from(frame).doOnNext(f -> LOGGER.debug("sending -> " + f.toString())); + frame = Flux.from(frame).doOnNext(f -> LOGGER.debug("sending -> " + FrameUtil.toString(f))); } return source.send(frame); @@ -157,7 +158,7 @@ public Mono send(Publisher frame) { @Override public Mono sendOne(ByteBuf frame) { if (debugEnabled) { - LOGGER.debug("sending -> " + frame.toString()); + LOGGER.debug("sending -> " + FrameUtil.toString(frame)); } return source.sendOne(frame); @@ -168,7 +169,7 @@ public Flux receive() { return processor.flatMapMany( f -> { if (debugEnabled) { - return f.doOnNext(frame -> LOGGER.debug("receiving -> " + frame.toString())); + return f.doOnNext(frame -> LOGGER.debug("receiving -> " + FrameUtil.toString(frame))); } else { return f; } diff --git a/rsocket-core/src/main/java/io/rsocket/transport/ClientTransport.java b/rsocket-core/src/main/java/io/rsocket/transport/ClientTransport.java index d5a8fe775..25fd67097 100644 --- a/rsocket-core/src/main/java/io/rsocket/transport/ClientTransport.java +++ b/rsocket-core/src/main/java/io/rsocket/transport/ClientTransport.java @@ -26,7 +26,8 @@ public interface ClientTransport extends Transport { * Returns a {@code Publisher}, every subscription to which returns a single {@code * DuplexConnection}. * + * @param mtu The mtu used for fragmentation - if set to zero fragmentation will be disabled * @return {@code Publisher}, every subscription returns a single {@code DuplexConnection}. */ - Mono connect(); + Mono connect(int mtu); } diff --git a/rsocket-core/src/main/java/io/rsocket/transport/ServerTransport.java b/rsocket-core/src/main/java/io/rsocket/transport/ServerTransport.java index 28af3fd4c..3adc90cc8 100644 --- a/rsocket-core/src/main/java/io/rsocket/transport/ServerTransport.java +++ b/rsocket-core/src/main/java/io/rsocket/transport/ServerTransport.java @@ -29,10 +29,11 @@ public interface ServerTransport extends Transport { * Starts this server. * * @param acceptor An acceptor to process a newly accepted {@code DuplexConnection} + * @param mtu The mtu used for fragmentation - if set to zero fragmentation will be disabled * @return A handle to retrieve information about a started server. * @throws NullPointerException if {@code acceptor} is {@code null} */ - Mono start(ConnectionAcceptor acceptor); + Mono start(ConnectionAcceptor acceptor, int mtu); /** A contract to accept a new {@code DuplexConnection}. */ interface ConnectionAcceptor extends Function> { diff --git a/rsocket-core/src/main/java/io/rsocket/uri/UriTransportRegistry.java b/rsocket-core/src/main/java/io/rsocket/uri/UriTransportRegistry.java index 5275d2304..204c5d1ea 100644 --- a/rsocket-core/src/main/java/io/rsocket/uri/UriTransportRegistry.java +++ b/rsocket-core/src/main/java/io/rsocket/uri/UriTransportRegistry.java @@ -34,9 +34,9 @@ */ public class UriTransportRegistry { private static final ClientTransport FAILED_CLIENT_LOOKUP = - () -> Mono.error(new UnsupportedOperationException()); + (mtu) -> Mono.error(new UnsupportedOperationException()); private static final ServerTransport FAILED_SERVER_LOOKUP = - acceptor -> Mono.error(new UnsupportedOperationException()); + (acceptor, mtu) -> Mono.error(new UnsupportedOperationException()); private List handlers; @@ -55,6 +55,10 @@ public static ClientTransport clientForUri(String uri) { return UriTransportRegistry.fromServices().findClient(uri); } + public static ServerTransport serverForUri(String uri) { + return UriTransportRegistry.fromServices().findServer(uri); + } + private ClientTransport findClient(String uriString) { URI uri = URI.create(uriString); @@ -68,10 +72,6 @@ private ClientTransport findClient(String uriString) { return FAILED_CLIENT_LOOKUP; } - public static ServerTransport serverForUri(String uri) { - return UriTransportRegistry.fromServices().findServer(uri); - } - private ServerTransport findServer(String uriString) { URI uri = URI.create(uriString); diff --git a/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java b/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java index 0b33e5e22..b91cf8ac6 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java @@ -45,62 +45,6 @@ private ByteBufPayload(final Handle handle) { this.handle = handle; } - @Override - public boolean hasMetadata() { - return metadata != null; - } - - @Override - public ByteBuf sliceMetadata() { - return metadata == null ? Unpooled.EMPTY_BUFFER : metadata.slice(); - } - - @Override - public ByteBuf sliceData() { - return data.slice(); - } - - @Override - public ByteBufPayload retain() { - super.retain(); - return this; - } - - @Override - public ByteBufPayload retain(int increment) { - super.retain(increment); - return this; - } - - @Override - public ByteBufPayload touch() { - data.touch(); - if (metadata != null) { - metadata.touch(); - } - return this; - } - - @Override - public ByteBufPayload touch(Object hint) { - data.touch(hint); - if (metadata != null) { - metadata.touch(hint); - } - return this; - } - - @Override - protected void deallocate() { - data.release(); - data = null; - if (metadata != null) { - metadata.release(); - metadata = null; - } - handle.recycle(this); - } - /** * Static factory method for a text payload. Mainly looks better than "new ByteBufPayload(data)" * @@ -179,4 +123,70 @@ public static Payload create(Payload payload) { payload.sliceData().retain(), payload.hasMetadata() ? payload.sliceMetadata().retain() : null); } + + @Override + public boolean hasMetadata() { + return metadata != null; + } + + @Override + public ByteBuf sliceMetadata() { + return metadata == null ? Unpooled.EMPTY_BUFFER : metadata.slice(); + } + + @Override + public ByteBuf data() { + return data; + } + + @Override + public ByteBuf metadata() { + return metadata == null ? Unpooled.EMPTY_BUFFER : metadata; + } + + @Override + public ByteBuf sliceData() { + return data.slice(); + } + + @Override + public ByteBufPayload retain() { + super.retain(); + return this; + } + + @Override + public ByteBufPayload retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public ByteBufPayload touch() { + data.touch(); + if (metadata != null) { + metadata.touch(); + } + return this; + } + + @Override + public ByteBufPayload touch(Object hint) { + data.touch(hint); + if (metadata != null) { + metadata.touch(hint); + } + return this; + } + + @Override + protected void deallocate() { + data.release(); + data = null; + if (metadata != null) { + metadata.release(); + metadata = null; + } + handle.recycle(this); + } } diff --git a/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java b/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java index 71bbf3874..ec73399f1 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java @@ -40,66 +40,6 @@ private DefaultPayload(ByteBuffer data, @Nullable ByteBuffer metadata) { this.metadata = metadata; } - @Override - public boolean hasMetadata() { - return metadata != null; - } - - @Override - public ByteBuf sliceMetadata() { - return metadata == null ? Unpooled.EMPTY_BUFFER : Unpooled.wrappedBuffer(metadata); - } - - @Override - public ByteBuf sliceData() { - return Unpooled.wrappedBuffer(data); - } - - @Override - public ByteBuffer getMetadata() { - return metadata == null ? DefaultPayload.EMPTY_BUFFER : metadata.duplicate(); - } - - @Override - public ByteBuffer getData() { - return data.duplicate(); - } - - @Override - public int refCnt() { - return 1; - } - - @Override - public DefaultPayload retain() { - return this; - } - - @Override - public DefaultPayload retain(int increment) { - return this; - } - - @Override - public DefaultPayload touch() { - return this; - } - - @Override - public DefaultPayload touch(Object hint) { - return this; - } - - @Override - public boolean release() { - return false; - } - - @Override - public boolean release(int decrement) { - return false; - } - /** * Static factory method for a text payload. Mainly looks better than "new DefaultPayload(data)" * @@ -167,4 +107,74 @@ public static Payload create(Payload payload) { Unpooled.copiedBuffer(payload.sliceData()), payload.hasMetadata() ? Unpooled.copiedBuffer(payload.sliceMetadata()) : null); } + + @Override + public boolean hasMetadata() { + return metadata != null; + } + + @Override + public ByteBuf sliceMetadata() { + return metadata == null ? Unpooled.EMPTY_BUFFER : Unpooled.wrappedBuffer(metadata); + } + + @Override + public ByteBuf sliceData() { + return Unpooled.wrappedBuffer(data); + } + + @Override + public ByteBuffer getMetadata() { + return metadata == null ? DefaultPayload.EMPTY_BUFFER : metadata.duplicate(); + } + + @Override + public ByteBuffer getData() { + return data.duplicate(); + } + + @Override + public ByteBuf data() { + return sliceData(); + } + + @Override + public ByteBuf metadata() { + return sliceMetadata(); + } + + @Override + public int refCnt() { + return 1; + } + + @Override + public DefaultPayload retain() { + return this; + } + + @Override + public DefaultPayload retain(int increment) { + return this; + } + + @Override + public DefaultPayload touch() { + return this; + } + + @Override + public DefaultPayload touch(Object hint) { + return this; + } + + @Override + public boolean release() { + return false; + } + + @Override + public boolean release(int decrement) { + return false; + } } diff --git a/rsocket-core/src/main/java/io/rsocket/util/EmptyPayload.java b/rsocket-core/src/main/java/io/rsocket/util/EmptyPayload.java index d5eda1d6b..99df97d70 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/EmptyPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/util/EmptyPayload.java @@ -40,6 +40,16 @@ public ByteBuf sliceData() { return Unpooled.EMPTY_BUFFER; } + @Override + public ByteBuf data() { + return sliceData(); + } + + @Override + public ByteBuf metadata() { + return sliceMetadata(); + } + @Override public int refCnt() { return 1; diff --git a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java index 2326f338d..57070ad69 100644 --- a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java @@ -124,7 +124,7 @@ private static class SingleConnectionTransport implements ServerTransport start(ConnectionAcceptor acceptor) { + public Mono start(ConnectionAcceptor acceptor, int mtu) { return Mono.just(new TestCloseable(acceptor, conn)); } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java index 6b25ac902..ac4413caa 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java @@ -16,27 +16,69 @@ package io.rsocket.fragmentation; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import static org.mockito.Mockito.*; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCountUtil; +import io.rsocket.DuplexConnection; +import io.rsocket.frame.*; +import io.rsocket.util.DefaultPayload; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.Assert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + final class FragmentationDuplexConnectionTest { - /* + private static byte[] data = new byte[1024]; + private static byte[] metadata = new byte[1024]; + + static { + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); + } + private final DuplexConnection delegate = mock(DuplexConnection.class, RETURNS_SMART_NULLS); @SuppressWarnings("unchecked") - private final ArgumentCaptor> publishers = + private final ArgumentCaptor> publishers = ArgumentCaptor.forClass(Publisher.class); + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + @DisplayName("constructor throws IllegalArgumentException with negative maxFragmentLength") @Test void constructorInvalidMaxFragmentSize() { assertThatIllegalArgumentException() - .isThrownBy(() -> new FragmentationDuplexConnection(DEFAULT, delegate, Integer.MIN_VALUE)) - .withMessage("maxFragmentSize must be positive"); + .isThrownBy( + () -> new FragmentationDuplexConnection(delegate, allocator, Integer.MIN_VALUE, false)) + .withMessage("smallest allowed mtu size is 64 bytes"); + } + + @DisplayName("constructor throws IllegalArgumentException with negative maxFragmentLength") + @Test + void constructorMtuLessThanMin() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new FragmentationDuplexConnection(delegate, allocator, 2, false)) + .withMessage("smallest allowed mtu size is 64 bytes"); } @DisplayName("constructor throws NullPointerException with null byteBufAllocator") @Test void constructorNullByteBufAllocator() { assertThatNullPointerException() - .isThrownBy(() -> new FragmentationDuplexConnection(null, delegate, 2)) + .isThrownBy(() -> new FragmentationDuplexConnection(delegate, null, 64, false)) .withMessage("byteBufAllocator must not be null"); } @@ -44,339 +86,241 @@ void constructorNullByteBufAllocator() { @Test void constructorNullDelegate() { assertThatNullPointerException() - .isThrownBy(() -> new FragmentationDuplexConnection(DEFAULT, null, 2)) + .isThrownBy(() -> new FragmentationDuplexConnection(null, allocator, 64, false)) .withMessage("delegate must not be null"); } @DisplayName("reassembles data") @Test void reassembleData() { - ByteBuf data = getRandomByteBuf(6); - - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, null, data)); - - Frame fragment1 = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, null, data.slice(0, 2))); - - Frame fragment2 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, false, null, data.slice(2, 2))); - - Frame fragment3 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, data.slice(4, 2))); - - when(delegate.receive()).thenReturn(Flux.just(fragment1, fragment2, fragment3)); + List byteBufs = + Arrays.asList( + RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)), + PayloadFrameFlyweight.encode( + allocator, 1, true, false, true, DefaultPayload.create(data)), + PayloadFrameFlyweight.encode( + allocator, 1, true, false, true, DefaultPayload.create(data)), + PayloadFrameFlyweight.encode( + allocator, 1, true, false, true, DefaultPayload.create(data)), + PayloadFrameFlyweight.encode( + allocator, 1, false, false, true, DefaultPayload.create(data))); + + CompositeByteBuf data = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data), + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data), + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data), + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data), + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data)); + + when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); when(delegate.onClose()).thenReturn(Mono.never()); - new FragmentationDuplexConnection(DEFAULT, delegate, 2) + new FragmentationDuplexConnection(delegate, allocator, 1030, false) .receive() .as(StepVerifier::create) - .expectNext(frame) + .assertNext( + byteBuf -> { + Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); + ReferenceCountUtil.safeRelease(byteBuf); + }) .verifyComplete(); } @DisplayName("reassembles metadata") @Test void reassembleMetadata() { - ByteBuf metadata = getRandomByteBuf(6); - - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, metadata, null)); - - Frame fragment1 = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null)); - - Frame fragment2 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null)); - - Frame fragment3 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, true, metadata.slice(4, 2), null)); - - when(delegate.receive()).thenReturn(Flux.just(fragment1, fragment2, fragment3)); + List byteBufs = + Arrays.asList( + RequestResponseFrameFlyweight.encode( + allocator, + 1, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + false, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata)))); + + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata)); + + when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); when(delegate.onClose()).thenReturn(Mono.never()); - new FragmentationDuplexConnection(DEFAULT, delegate, 2) + new FragmentationDuplexConnection(delegate, allocator, 1030, false) .receive() .as(StepVerifier::create) - .expectNext(frame) + .assertNext( + byteBuf -> { + System.out.println(byteBuf.readableBytes()); + ByteBuf m = RequestResponseFrameFlyweight.metadata(byteBuf); + Assert.assertEquals(metadata, m); + }) .verifyComplete(); } @DisplayName("reassembles metadata and data") @Test void reassembleMetadataAndData() { - ByteBuf metadata = getRandomByteBuf(5); - ByteBuf data = getRandomByteBuf(5); - - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, metadata, data)); - - Frame fragment1 = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null)); - - Frame fragment2 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null)); - - Frame fragment3 = - toAbstractionLeakingFrame( - DEFAULT, - 1, - createPayloadFrame(DEFAULT, true, false, metadata.slice(4, 1), data.slice(0, 1))); - - Frame fragment4 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, false, null, data.slice(1, 2))); - - Frame fragment5 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, data.slice(3, 2))); - - when(delegate.receive()) - .thenReturn(Flux.just(fragment1, fragment2, fragment3, fragment4, fragment5)); + List byteBufs = + Arrays.asList( + RequestResponseFrameFlyweight.encode( + allocator, + 1, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create( + Unpooled.wrappedBuffer(data), Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, 1, false, false, true, DefaultPayload.create(data))); + + CompositeByteBuf data = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data), + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data)); + + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata)); + + when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); when(delegate.onClose()).thenReturn(Mono.never()); - new FragmentationDuplexConnection(DEFAULT, delegate, 2) + new FragmentationDuplexConnection(delegate, allocator, 1030, false) .receive() .as(StepVerifier::create) - .expectNext(frame) + .assertNext( + byteBuf -> { + Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); + Assert.assertEquals(metadata, RequestResponseFrameFlyweight.metadata(byteBuf)); + }) .verifyComplete(); } @DisplayName("does not reassemble a non-fragment frame") @Test void reassembleNonFragment() { - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, true, (ByteBuf) null, null)); + ByteBuf encode = + RequestResponseFrameFlyweight.encode( + allocator, 1, false, DefaultPayload.create(Unpooled.wrappedBuffer(data))); - when(delegate.receive()).thenReturn(Flux.just(frame.retain())); + when(delegate.receive()).thenReturn(Flux.just(encode)); when(delegate.onClose()).thenReturn(Mono.never()); - new FragmentationDuplexConnection(DEFAULT, delegate, 2) + new FragmentationDuplexConnection(delegate, allocator, 1030, false) .receive() .as(StepVerifier::create) - .expectNext(frame) + .assertNext( + byteBuf -> { + Assert.assertEquals( + Unpooled.wrappedBuffer(data), RequestResponseFrameFlyweight.data(byteBuf)); + }) .verifyComplete(); } @DisplayName("does not reassemble non fragmentable frame") @Test void reassembleNonFragmentableFrame() { - Frame frame = toAbstractionLeakingFrame(DEFAULT, 1, createTestCancelFrame()); + ByteBuf encode = CancelFrameFlyweight.encode(allocator, 2); - when(delegate.receive()).thenReturn(Flux.just(frame.retain())); + when(delegate.receive()).thenReturn(Flux.just(encode)); when(delegate.onClose()).thenReturn(Mono.never()); - new FragmentationDuplexConnection(DEFAULT, delegate, 2) + new FragmentationDuplexConnection(delegate, allocator, 1030, false) .receive() .as(StepVerifier::create) - .expectNext(frame) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.CANCEL, FrameHeaderFlyweight.frameType(byteBuf)); + }) .verifyComplete(); } @DisplayName("fragments data") @Test void sendData() { - ByteBuf data = getRandomByteBuf(6); - - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, null, data)); - - Frame fragment1 = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, null, data.slice(0, 2))); - - Frame fragment2 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, false, null, data.slice(2, 2))); - - Frame fragment3 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, data.slice(4, 2))); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())) - .expectNext(fragment1) - .expectNext(fragment2) - .expectNext(fragment3) - .verifyComplete(); - } - - @DisplayName("does not fragment with size equal to maxFragmentLength") - @Test - void sendEqualToMaxFragmentLength() { - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(2))); + ByteBuf encode = + RequestResponseFrameFlyweight.encode( + allocator, 1, false, Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(data)); when(delegate.onClose()).thenReturn(Mono.never()); - new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).verifyComplete(); - } + new FragmentationDuplexConnection(delegate, allocator, 64, false).sendOne(encode.retain()); - @DisplayName("does not fragment an already-fragmented frame") - @Test - void sendFragment() { - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, (ByteBuf) null, null)); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).verifyComplete(); - } - - @DisplayName("does not fragment with size smaller than maxFragmentLength") - @Test - void sendLessThanMaxFragmentLength() { - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(1))); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).verifyComplete(); - } - - @DisplayName("fragments metadata") - @Test - void sendMetadata() { - ByteBuf metadata = getRandomByteBuf(6); - - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, metadata, null)); - - Frame fragment1 = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null)); - - Frame fragment2 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null)); - - Frame fragment3 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, true, metadata.slice(4, 2), null)); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())) - .expectNext(fragment1) - .expectNext(fragment2) - .expectNext(fragment3) - .verifyComplete(); - } - - @DisplayName("fragments metadata and data") - @Test - void sendMetadataAndData() { - ByteBuf metadata = getRandomByteBuf(5); - ByteBuf data = getRandomByteBuf(5); - - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, false, 1, metadata, data)); - - Frame fragment1 = - toAbstractionLeakingFrame( - DEFAULT, 1, createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null)); - - Frame fragment2 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null)); - - Frame fragment3 = - toAbstractionLeakingFrame( - DEFAULT, - 1, - createPayloadFrame(DEFAULT, true, false, metadata.slice(4, 1), data.slice(0, 1))); - - Frame fragment4 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, true, false, null, data.slice(1, 2))); - - Frame fragment5 = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, data.slice(3, 2))); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame.retain()); verify(delegate).send(publishers.capture()); StepVerifier.create(Flux.from(publishers.getValue())) - .expectNext(fragment1) - .expectNext(fragment2) - .expectNext(fragment3) - .expectNext(fragment4) - .expectNext(fragment5) + .expectNextCount(17) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); + Assert.assertFalse(FrameHeaderFlyweight.hasFollows(byteBuf)); + }) .verifyComplete(); } - - @DisplayName("does not fragment non-fragmentable frame") - @Test - void sendNonFragmentable() { - Frame frame = toAbstractionLeakingFrame(DEFAULT, 1, createTestCancelFrame()); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 2).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).verifyComplete(); - } - - @DisplayName("send throws NullPointerException with null frames") - @Test - void sendNullFrames() { - when(delegate.onClose()).thenReturn(Mono.never()); - - assertThatNullPointerException() - .isThrownBy(() -> new FragmentationDuplexConnection(DEFAULT, delegate, 2).send(null)) - .withMessage("frames must not be null"); - } - - @DisplayName("does not fragment with zero maxFragmentLength") - @Test - void sendZeroMaxFragmentLength() { - Frame frame = - toAbstractionLeakingFrame( - DEFAULT, 1, createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(2))); - - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(DEFAULT, delegate, 0).sendOne(frame.retain()); - verify(delegate).send(publishers.capture()); - - StepVerifier.create(Flux.from(publishers.getValue())).expectNext(frame).verifyComplete(); - }*/ } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java new file mode 100644 index 000000000..df68da9a5 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java @@ -0,0 +1,55 @@ +package io.rsocket.fragmentation; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameUtil; +import io.rsocket.frame.PayloadFrameFlyweight; +import io.rsocket.util.DefaultPayload; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.Assert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +public class FragmentationIntegrationTest { + private static byte[] data = new byte[128]; + private static byte[] metadata = new byte[128]; + + static { + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); + } + + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + + @DisplayName("fragments and reassembles data") + @Test + void fragmentAndReassembleData() { + ByteBuf frame = + PayloadFrameFlyweight.encodeNextComplete(allocator, 2, DefaultPayload.create(data)); + System.out.println(FrameUtil.toString(frame)); + + Publisher fragments = + FrameFragmenter.fragmentFrame( + allocator, 64, frame, FrameHeaderFlyweight.frameType(frame), false); + + FrameReassembler reassembler = new FrameReassembler(allocator); + + ByteBuf assembled = + Flux.from(fragments) + .doOnNext(byteBuf -> System.out.println(FrameUtil.toString(byteBuf))) + .handle(reassembler::reassembleFrame) + .blockLast(); + + System.out.println("assembled"); + String s = FrameUtil.toString(assembled); + System.out.println(s); + + Assert.assertEquals( + FrameHeaderFlyweight.frameType(frame), FrameHeaderFlyweight.frameType(assembled)); + Assert.assertEquals(frame.readableBytes(), assembled.readableBytes()); + Assert.assertEquals(PayloadFrameFlyweight.data(frame), PayloadFrameFlyweight.data(assembled)); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java index 8cf3edb96..f5a013357 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java @@ -16,158 +16,337 @@ package io.rsocket.fragmentation; -final class FrameFragmenterTest { - /* - @DisplayName("constructor throws NullPointerException with null ByteBufAllocator") - @Test - void constructorNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> new FrameFragmenter(null, 2)) - .withMessage("byteBufAllocator must not be null"); - } - - @DisplayName("fragments data") - @Test - void fragmentData() { - ByteBuf data = getRandomByteBuf(6); - - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, null, data); +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.frame.*; +import io.rsocket.util.DefaultPayload; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.Assert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; - RequestStreamFrame fragment1 = - createRequestStreamFrame(DEFAULT, true, 1, null, data.slice(0, 2)); +final class FrameFragmenterTest { + private static byte[] data = new byte[4096]; + private static byte[] metadata = new byte[4096]; - PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, false, null, data.slice(2, 2)); + static { + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); + } - PayloadFrame fragment3 = createPayloadFrame(DEFAULT, false, false, null, data.slice(4, 2)); + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(fragment1) - .expectNext(fragment2) - .expectNext(fragment3) - .verifyComplete(); + @Test + void testGettingData() { + ByteBuf rr = + RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)); + ByteBuf fnf = + RequestFireAndForgetFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)); + ByteBuf rs = + RequestStreamFrameFlyweight.encode(allocator, 1, true, 1, DefaultPayload.create(data)); + ByteBuf rc = + RequestChannelFrameFlyweight.encode( + allocator, 1, true, false, 1, DefaultPayload.create(data)); + + ByteBuf data = FrameFragmenter.getData(rr, FrameType.REQUEST_RESPONSE); + Assert.assertEquals(data, Unpooled.wrappedBuffer(data)); + data.release(); + + data = FrameFragmenter.getData(fnf, FrameType.REQUEST_FNF); + Assert.assertEquals(data, Unpooled.wrappedBuffer(data)); + data.release(); + + data = FrameFragmenter.getData(rs, FrameType.REQUEST_STREAM); + Assert.assertEquals(data, Unpooled.wrappedBuffer(data)); + data.release(); + + data = FrameFragmenter.getData(rc, FrameType.REQUEST_CHANNEL); + Assert.assertEquals(data, Unpooled.wrappedBuffer(data)); + data.release(); } - @DisplayName("does not fragment with size equal to maxFragmentLength") @Test - void fragmentEqualToMaxFragmentLength() { - PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(2)); - - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(frame) - .verifyComplete(); + void testGettingMetadata() { + ByteBuf rr = + RequestResponseFrameFlyweight.encode( + allocator, 1, true, DefaultPayload.create(data, metadata)); + ByteBuf fnf = + RequestFireAndForgetFrameFlyweight.encode( + allocator, 1, true, DefaultPayload.create(data, metadata)); + ByteBuf rs = + RequestStreamFrameFlyweight.encode( + allocator, 1, true, 1, DefaultPayload.create(data, metadata)); + ByteBuf rc = + RequestChannelFrameFlyweight.encode( + allocator, 1, true, false, 1, DefaultPayload.create(data, metadata)); + + ByteBuf data = FrameFragmenter.getMetadata(rr, FrameType.REQUEST_RESPONSE); + Assert.assertEquals(data, Unpooled.wrappedBuffer(metadata)); + data.release(); + + data = FrameFragmenter.getMetadata(fnf, FrameType.REQUEST_FNF); + Assert.assertEquals(data, Unpooled.wrappedBuffer(metadata)); + data.release(); + + data = FrameFragmenter.getMetadata(rs, FrameType.REQUEST_STREAM); + Assert.assertEquals(data, Unpooled.wrappedBuffer(metadata)); + data.release(); + + data = FrameFragmenter.getMetadata(rc, FrameType.REQUEST_CHANNEL); + Assert.assertEquals(data, Unpooled.wrappedBuffer(metadata)); + data.release(); } - @DisplayName("does not fragment an already-fragmented frame") @Test - void fragmentFragment() { - PayloadFrame frame = createPayloadFrame(DEFAULT, true, true, (ByteBuf) null, null); + void returnEmptBufferWhenNoMetadataPresent() { + ByteBuf rr = + RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)); - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(frame) - .verifyComplete(); + ByteBuf data = FrameFragmenter.getMetadata(rr, FrameType.REQUEST_RESPONSE); + Assert.assertEquals(data, Unpooled.EMPTY_BUFFER); + data.release(); } - @DisplayName("does not fragment with size smaller than maxFragmentLength") + @DisplayName("encode first frame") @Test - void fragmentLessThanMaxFragmentLength() { - PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(1)); - - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(frame) - .verifyComplete(); + void encodeFirstFrameWithData() { + ByteBuf rr = + RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)); + + ByteBuf fragment = + FrameFragmenter.encodeFirstFragment( + allocator, + 256, + rr, + FrameType.REQUEST_RESPONSE, + 1, + Unpooled.EMPTY_BUFFER, + Unpooled.wrappedBuffer(data)); + + Assert.assertEquals(256, fragment.readableBytes()); + Assert.assertEquals(FrameType.REQUEST_RESPONSE, FrameHeaderFlyweight.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderFlyweight.streamId(fragment)); + Assert.assertTrue(FrameHeaderFlyweight.hasFollows(fragment)); + + ByteBuf data = RequestResponseFrameFlyweight.data(fragment); + ByteBuf byteBuf = Unpooled.wrappedBuffer(this.data).readSlice(data.readableBytes()); + Assert.assertEquals(byteBuf, data); + + Assert.assertFalse(FrameHeaderFlyweight.hasMetadata(fragment)); } - @DisplayName("fragments metadata") + @DisplayName("encode first channel frame") @Test - void fragmentMetadata() { - ByteBuf metadata = getRandomByteBuf(6); - - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, metadata, null); - - RequestStreamFrame fragment1 = - createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null); - - PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null); - - PayloadFrame fragment3 = createPayloadFrame(DEFAULT, false, true, metadata.slice(4, 2), null); - - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(fragment1) - .expectNext(fragment2) - .expectNext(fragment3) - .verifyComplete(); + void encodeFirstWithDataChannel() { + ByteBuf rc = + RequestChannelFrameFlyweight.encode( + allocator, 1, true, false, 10, DefaultPayload.create(data)); + + ByteBuf fragment = + FrameFragmenter.encodeFirstFragment( + allocator, + 256, + rc, + FrameType.REQUEST_CHANNEL, + 1, + Unpooled.EMPTY_BUFFER, + Unpooled.wrappedBuffer(data)); + + Assert.assertEquals(256, fragment.readableBytes()); + Assert.assertEquals(FrameType.REQUEST_CHANNEL, FrameHeaderFlyweight.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderFlyweight.streamId(fragment)); + Assert.assertEquals(10, RequestChannelFrameFlyweight.initialRequestN(fragment)); + Assert.assertTrue(FrameHeaderFlyweight.hasFollows(fragment)); + + ByteBuf data = RequestChannelFrameFlyweight.data(fragment); + ByteBuf byteBuf = Unpooled.wrappedBuffer(this.data).readSlice(data.readableBytes()); + Assert.assertEquals(byteBuf, data); + + Assert.assertFalse(FrameHeaderFlyweight.hasMetadata(fragment)); } - @DisplayName("fragments metadata and data") + @DisplayName("encode first stream frame") @Test - void fragmentMetadataAndData() { - ByteBuf metadata = getRandomByteBuf(5); - ByteBuf data = getRandomByteBuf(5); - - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, metadata, data); - - RequestStreamFrame fragment1 = - createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null); - - PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null); - - PayloadFrame fragment3 = - createPayloadFrame(DEFAULT, true, false, metadata.slice(4, 1), data.slice(0, 1)); - - PayloadFrame fragment4 = createPayloadFrame(DEFAULT, true, false, null, data.slice(1, 2)); - - PayloadFrame fragment5 = createPayloadFrame(DEFAULT, false, false, null, data.slice(3, 2)); + void encodeFirstWithDataStream() { + ByteBuf rc = + RequestStreamFrameFlyweight.encode(allocator, 1, true, 50, DefaultPayload.create(data)); + + ByteBuf fragment = + FrameFragmenter.encodeFirstFragment( + allocator, + 256, + rc, + FrameType.REQUEST_STREAM, + 1, + Unpooled.EMPTY_BUFFER, + Unpooled.wrappedBuffer(data)); + + Assert.assertEquals(256, fragment.readableBytes()); + Assert.assertEquals(FrameType.REQUEST_STREAM, FrameHeaderFlyweight.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderFlyweight.streamId(fragment)); + Assert.assertEquals(50, RequestStreamFrameFlyweight.initialRequestN(fragment)); + Assert.assertTrue(FrameHeaderFlyweight.hasFollows(fragment)); + + ByteBuf data = RequestStreamFrameFlyweight.data(fragment); + ByteBuf byteBuf = Unpooled.wrappedBuffer(this.data).readSlice(data.readableBytes()); + Assert.assertEquals(byteBuf, data); + + Assert.assertFalse(FrameHeaderFlyweight.hasMetadata(fragment)); + } - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(fragment1) - .expectNext(fragment2) - .expectNext(fragment3) - .expectNext(fragment4) - .expectNext(fragment5) - .verifyComplete(); + @DisplayName("encode first frame with only metadata") + @Test + void encodeFirstFrameWithMetadata() { + ByteBuf rr = + RequestResponseFrameFlyweight.encode( + allocator, + 1, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))); + + ByteBuf fragment = + FrameFragmenter.encodeFirstFragment( + allocator, + 256, + rr, + FrameType.REQUEST_RESPONSE, + 1, + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER); + + Assert.assertEquals(256, fragment.readableBytes()); + Assert.assertEquals(FrameType.REQUEST_RESPONSE, FrameHeaderFlyweight.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderFlyweight.streamId(fragment)); + Assert.assertTrue(FrameHeaderFlyweight.hasFollows(fragment)); + + ByteBuf data = RequestResponseFrameFlyweight.data(fragment); + Assert.assertEquals(data, Unpooled.EMPTY_BUFFER); + + Assert.assertTrue(FrameHeaderFlyweight.hasMetadata(fragment)); } - @DisplayName("does not fragment non-fragmentable frame") + @DisplayName("encode first stream frame with data and metadata") @Test - void fragmentNonFragmentable() { - CancelFrame frame = createTestCancelFrame(); + void encodeFirstWithDataAndMetadataStream() { + ByteBuf rc = + RequestStreamFrameFlyweight.encode( + allocator, 1, true, 50, DefaultPayload.create(data, metadata)); + + ByteBuf fragment = + FrameFragmenter.encodeFirstFragment( + allocator, + 256, + rc, + FrameType.REQUEST_STREAM, + 1, + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data)); + + Assert.assertEquals(256, fragment.readableBytes()); + Assert.assertEquals(FrameType.REQUEST_STREAM, FrameHeaderFlyweight.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderFlyweight.streamId(fragment)); + Assert.assertEquals(50, RequestStreamFrameFlyweight.initialRequestN(fragment)); + Assert.assertTrue(FrameHeaderFlyweight.hasFollows(fragment)); + + ByteBuf data = RequestStreamFrameFlyweight.data(fragment); + Assert.assertEquals(0, data.readableBytes()); + + ByteBuf metadata = RequestStreamFrameFlyweight.metadata(fragment); + ByteBuf byteBuf = Unpooled.wrappedBuffer(this.metadata).readSlice(metadata.readableBytes()); + Assert.assertEquals(byteBuf, metadata); + + Assert.assertTrue(FrameHeaderFlyweight.hasMetadata(fragment)); + } - new FrameFragmenter(DEFAULT, 2) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(frame) + @DisplayName("fragments frame with only data") + @Test + void fragmentData() { + ByteBuf rr = + RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)); + + Publisher fragments = + FrameFragmenter.fragmentFrame(allocator, 1024, rr, FrameType.REQUEST_RESPONSE, false); + + StepVerifier.create(Flux.from(fragments).doOnError(Throwable::printStackTrace)) + .expectNextCount(1) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); + Assert.assertEquals(1, FrameHeaderFlyweight.streamId(byteBuf)); + Assert.assertTrue(FrameHeaderFlyweight.hasFollows(byteBuf)); + }) + .expectNextCount(2) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); + Assert.assertFalse(FrameHeaderFlyweight.hasFollows(byteBuf)); + }) .verifyComplete(); } - @DisplayName("fragment throws NullPointerException with null frame") + @DisplayName("fragments frame with only metadata") @Test - void fragmentWithNullFrame() { - assertThatNullPointerException() - .isThrownBy(() -> new FrameFragmenter(DEFAULT, 2).fragment(null)) - .withMessage("frame must not be null"); + void fragmentMetadata() { + ByteBuf rr = + RequestStreamFrameFlyweight.encode( + allocator, + 1, + true, + 10, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))); + + Publisher fragments = + FrameFragmenter.fragmentFrame(allocator, 1024, rr, FrameType.REQUEST_STREAM, false); + + StepVerifier.create(Flux.from(fragments).doOnError(Throwable::printStackTrace)) + .expectNextCount(1) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); + Assert.assertEquals(1, FrameHeaderFlyweight.streamId(byteBuf)); + Assert.assertTrue(FrameHeaderFlyweight.hasFollows(byteBuf)); + }) + .expectNextCount(2) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); + Assert.assertFalse(FrameHeaderFlyweight.hasFollows(byteBuf)); + }) + .verifyComplete(); } - @DisplayName("does not fragment with zero maxFragmentLength") + @DisplayName("fragments frame with data and metadata") @Test - void fragmentZeroMaxFragmentLength() { - PayloadFrame frame = createPayloadFrame(DEFAULT, false, false, null, getRandomByteBuf(2)); - - new FrameFragmenter(DEFAULT, 0) - .fragment(frame) - .as(StepVerifier::create) - .expectNext(frame) + void fragmentDataAndMetadata() { + ByteBuf rr = + RequestResponseFrameFlyweight.encode( + allocator, 1, true, DefaultPayload.create(data, metadata)); + + Publisher fragments = + FrameFragmenter.fragmentFrame(allocator, 1024, rr, FrameType.REQUEST_RESPONSE, false); + + StepVerifier.create(Flux.from(fragments).doOnError(Throwable::printStackTrace)) + .assertNext( + byteBuf -> { + Assert.assertEquals( + FrameType.REQUEST_RESPONSE, FrameHeaderFlyweight.frameType(byteBuf)); + Assert.assertTrue(FrameHeaderFlyweight.hasFollows(byteBuf)); + }) + .expectNextCount(6) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); + Assert.assertTrue(FrameHeaderFlyweight.hasFollows(byteBuf)); + }) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); + Assert.assertFalse(FrameHeaderFlyweight.hasFollows(byteBuf)); + }) .verifyComplete(); - }*/ + } } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java index 467f6c2e7..6e0d0dc1b 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java @@ -16,108 +16,464 @@ package io.rsocket.fragmentation; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCountUtil; +import io.rsocket.frame.*; +import io.rsocket.util.DefaultPayload; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.Assert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + final class FrameReassemblerTest { - /* - @DisplayName("createFrameReassembler throws NullPointerException") - @Test - void createFrameReassemblerNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> createFrameReassembler(null)) - .withMessage("byteBufAllocator must not be null"); + private static byte[] data = new byte[1024]; + private static byte[] metadata = new byte[1024]; + + static { + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); } + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + @DisplayName("reassembles data") @Test void reassembleData() { - ByteBuf data = getRandomByteBuf(6); + List byteBufs = + Arrays.asList( + RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)), + PayloadFrameFlyweight.encode( + allocator, 1, true, false, true, DefaultPayload.create(data)), + PayloadFrameFlyweight.encode( + allocator, 1, true, false, true, DefaultPayload.create(data)), + PayloadFrameFlyweight.encode( + allocator, 1, true, false, true, DefaultPayload.create(data)), + PayloadFrameFlyweight.encode( + allocator, 1, false, false, true, DefaultPayload.create(data))); + + FrameReassembler reassembler = new FrameReassembler(allocator); - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, null, data); + Flux assembled = Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame); - RequestStreamFrame fragment1 = - createRequestStreamFrame(DEFAULT, true, 1, null, data.slice(0, 2)); + CompositeByteBuf data = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FrameReassemblerTest.data), + Unpooled.wrappedBuffer(FrameReassemblerTest.data), + Unpooled.wrappedBuffer(FrameReassemblerTest.data), + Unpooled.wrappedBuffer(FrameReassemblerTest.data), + Unpooled.wrappedBuffer(FrameReassemblerTest.data)); + + StepVerifier.create(assembled) + .assertNext( + byteBuf -> { + Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); + ReferenceCountUtil.safeRelease(byteBuf); + }) + .verifyComplete(); + ReferenceCountUtil.safeRelease(data); + } + + @DisplayName("pass through frames without follows") + @Test + void passthrough() { + List byteBufs = + Arrays.asList( + RequestResponseFrameFlyweight.encode(allocator, 1, false, DefaultPayload.create(data))); - PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, false, null, data.slice(2, 2)); + FrameReassembler reassembler = new FrameReassembler(allocator); - PayloadFrame fragment3 = createPayloadFrame(DEFAULT, false, false, null, data.slice(4, 2)); + Flux assembled = Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame); - FrameReassembler frameReassembler = createFrameReassembler(DEFAULT); + CompositeByteBuf data = + allocator + .compositeDirectBuffer() + .addComponents(true, Unpooled.wrappedBuffer(FrameReassemblerTest.data)); - assertThat(frameReassembler.reassemble(fragment1)).isNull(); - assertThat(frameReassembler.reassemble(fragment2)).isNull(); - assertThat(frameReassembler.reassemble(fragment3)).isEqualTo(frame); + StepVerifier.create(assembled) + .assertNext( + byteBuf -> { + Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); + ReferenceCountUtil.safeRelease(byteBuf); + }) + .verifyComplete(); + ReferenceCountUtil.safeRelease(data); } @DisplayName("reassembles metadata") @Test void reassembleMetadata() { - ByteBuf metadata = getRandomByteBuf(6); + List byteBufs = + Arrays.asList( + RequestResponseFrameFlyweight.encode( + allocator, + 1, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + false, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata)))); - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, metadata, null); + FrameReassembler reassembler = new FrameReassembler(allocator); - RequestStreamFrame fragment1 = - createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null); + Flux assembled = Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame); - PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null); + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata)); - PayloadFrame fragment3 = createPayloadFrame(DEFAULT, false, true, metadata.slice(4, 2), null); - - FrameReassembler frameReassembler = createFrameReassembler(DEFAULT); - - assertThat(frameReassembler.reassemble(fragment1)).isNull(); - assertThat(frameReassembler.reassemble(fragment2)).isNull(); - assertThat(frameReassembler.reassemble(fragment3)).isEqualTo(frame); + StepVerifier.create(assembled) + .assertNext( + byteBuf -> { + System.out.println(byteBuf.readableBytes()); + ByteBuf m = RequestResponseFrameFlyweight.metadata(byteBuf); + Assert.assertEquals(metadata, m); + }) + .verifyComplete(); } - @DisplayName("reassembles metadata and data") + @DisplayName("reassembles metadata request channel") @Test - void reassembleMetadataAndData() { - ByteBuf metadata = getRandomByteBuf(5); - ByteBuf data = getRandomByteBuf(5); + void reassembleMetadataChannel() { + List byteBufs = + Arrays.asList( + RequestChannelFrameFlyweight.encode( + allocator, + 1, + true, + false, + 100, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + false, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata)))); - RequestStreamFrame frame = createRequestStreamFrame(DEFAULT, false, 1, metadata, data); + FrameReassembler reassembler = new FrameReassembler(allocator); - RequestStreamFrame fragment1 = - createRequestStreamFrame(DEFAULT, true, 1, metadata.slice(0, 2), null); + Flux assembled = Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame); - PayloadFrame fragment2 = createPayloadFrame(DEFAULT, true, true, metadata.slice(2, 2), null); + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata)); - PayloadFrame fragment3 = - createPayloadFrame(DEFAULT, true, false, metadata.slice(4, 1), data.slice(0, 1)); + StepVerifier.create(assembled) + .assertNext( + byteBuf -> { + System.out.println(byteBuf.readableBytes()); + ByteBuf m = RequestChannelFrameFlyweight.metadata(byteBuf); + Assert.assertEquals(metadata, m); + Assert.assertEquals(100, RequestChannelFrameFlyweight.initialRequestN(byteBuf)); + ReferenceCountUtil.safeRelease(byteBuf); + }) + .verifyComplete(); + + ReferenceCountUtil.safeRelease(metadata); + } + + @DisplayName("reassembles metadata request stream") + @Test + void reassembleMetadataStream() { + List byteBufs = + Arrays.asList( + RequestStreamFrameFlyweight.encode( + allocator, + 1, + true, + 250, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + false, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata)))); - PayloadFrame fragment4 = createPayloadFrame(DEFAULT, true, false, null, data.slice(1, 2)); + FrameReassembler reassembler = new FrameReassembler(allocator); - PayloadFrame fragment5 = createPayloadFrame(DEFAULT, false, false, null, data.slice(3, 2)); + Flux assembled = Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame); - FrameReassembler frameReassembler = createFrameReassembler(DEFAULT); + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata)); - assertThat(frameReassembler.reassemble(fragment1)).isNull(); - assertThat(frameReassembler.reassemble(fragment2)).isNull(); - assertThat(frameReassembler.reassemble(fragment3)).isNull(); - assertThat(frameReassembler.reassemble(fragment4)).isNull(); - assertThat(frameReassembler.reassemble(fragment5)).isEqualTo(frame); + StepVerifier.create(assembled) + .assertNext( + byteBuf -> { + System.out.println(byteBuf.readableBytes()); + ByteBuf m = RequestStreamFrameFlyweight.metadata(byteBuf); + Assert.assertEquals(metadata, m); + Assert.assertEquals(250, RequestChannelFrameFlyweight.initialRequestN(byteBuf)); + ReferenceCountUtil.safeRelease(byteBuf); + }) + .verifyComplete(); + + ReferenceCountUtil.safeRelease(metadata); } - @DisplayName("does not reassemble a non-fragment frame") + @DisplayName("reassembles metadata and data") @Test - void reassembleNonFragment() { - PayloadFrame frame = createPayloadFrame(DEFAULT, false, true, (ByteBuf) null, null); + void reassembleMetadataAndData() { + + List byteBufs = + Arrays.asList( + RequestResponseFrameFlyweight.encode( + allocator, + 1, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create( + Unpooled.wrappedBuffer(data), Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, 1, false, false, true, DefaultPayload.create(data))); + + FrameReassembler reassembler = new FrameReassembler(allocator); + + Flux assembled = Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame); + + CompositeByteBuf data = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FrameReassemblerTest.data), + Unpooled.wrappedBuffer(FrameReassemblerTest.data)); + + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata), + Unpooled.wrappedBuffer(FrameReassemblerTest.metadata)); - assertThat(createFrameReassembler(DEFAULT).reassemble(frame)).isEqualTo(frame); + StepVerifier.create(assembled) + .assertNext( + byteBuf -> { + Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); + Assert.assertEquals(metadata, RequestResponseFrameFlyweight.metadata(byteBuf)); + }) + .verifyComplete(); + ReferenceCountUtil.safeRelease(data); + ReferenceCountUtil.safeRelease(metadata); } - @DisplayName("does not reassemble non fragmentable frame") + @DisplayName("cancel removes inflight frames") @Test - void reassembleNonFragmentableFrame() { - CancelFrame frame = createTestCancelFrame(); + public void cancelBeforeAssembling() { + List byteBufs = + Arrays.asList( + RequestResponseFrameFlyweight.encode( + allocator, + 1, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create( + Unpooled.wrappedBuffer(data), Unpooled.wrappedBuffer(metadata)))); - assertThat(createFrameReassembler(DEFAULT).reassemble(frame)).isEqualTo(frame); + FrameReassembler reassembler = new FrameReassembler(allocator); + Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame).blockLast(); + + Assert.assertTrue(reassembler.headers.containsKey(1)); + Assert.assertTrue(reassembler.metadata.containsKey(1)); + Assert.assertTrue(reassembler.data.containsKey(1)); + + Flux.just(CancelFrameFlyweight.encode(allocator, 1)) + .handle(reassembler::reassembleFrame) + .blockLast(); + + Assert.assertFalse(reassembler.headers.containsKey(1)); + Assert.assertFalse(reassembler.metadata.containsKey(1)); + Assert.assertFalse(reassembler.data.containsKey(1)); } - @DisplayName("reassemble throws NullPointerException with null frame") + @DisplayName("dispose should clean up maps") @Test - void reassembleNullFrame() { - assertThatNullPointerException() - .isThrownBy(() -> createFrameReassembler(DEFAULT).reassemble(null)) - .withMessage("frame must not be null"); - }*/ + public void dispose() { + List byteBufs = + Arrays.asList( + RequestResponseFrameFlyweight.encode( + allocator, + 1, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create( + Unpooled.wrappedBuffer(data), Unpooled.wrappedBuffer(metadata)))); + + FrameReassembler reassembler = new FrameReassembler(allocator); + Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame).blockLast(); + + Assert.assertTrue(reassembler.headers.containsKey(1)); + Assert.assertTrue(reassembler.metadata.containsKey(1)); + Assert.assertTrue(reassembler.data.containsKey(1)); + + reassembler.dispose(); + + Assert.assertFalse(reassembler.headers.containsKey(1)); + Assert.assertFalse(reassembler.metadata.containsKey(1)); + Assert.assertFalse(reassembler.data.containsKey(1)); + } } diff --git a/rsocket-core/src/test/java/io/rsocket/uri/TestUriHandler.java b/rsocket-core/src/test/java/io/rsocket/uri/TestUriHandler.java index 46634e94b..526757fbe 100644 --- a/rsocket-core/src/test/java/io/rsocket/uri/TestUriHandler.java +++ b/rsocket-core/src/test/java/io/rsocket/uri/TestUriHandler.java @@ -36,7 +36,7 @@ public Optional buildClient(URI uri) { return Optional.empty(); } - return Optional.of(() -> Mono.just(new TestDuplexConnection())); + return Optional.of((mtu) -> Mono.just(new TestDuplexConnection())); } @Override diff --git a/rsocket-core/src/test/java/io/rsocket/uri/UriTransportRegistryTest.java b/rsocket-core/src/test/java/io/rsocket/uri/UriTransportRegistryTest.java index 9e7b92f65..7aeef708f 100644 --- a/rsocket-core/src/test/java/io/rsocket/uri/UriTransportRegistryTest.java +++ b/rsocket-core/src/test/java/io/rsocket/uri/UriTransportRegistryTest.java @@ -28,7 +28,7 @@ public class UriTransportRegistryTest { public void testTestRegistered() { ClientTransport test = UriTransportRegistry.clientForUri("test://test"); - DuplexConnection duplexConnection = test.connect().block(); + DuplexConnection duplexConnection = test.connect(0).block(); assertTrue(duplexConnection instanceof TestDuplexConnection); } @@ -37,6 +37,6 @@ public void testTestRegistered() { public void testTestUnregistered() { ClientTransport test = UriTransportRegistry.clientForUri("mailto://bonson@baulsupp.net"); - test.connect().block(); + test.connect(0).block(); } } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java index 91e8c3e57..55d6aaf93 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java @@ -17,7 +17,9 @@ package io.rsocket.transport.local; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; +import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.local.LocalServerTransport.ServerDuplexConnectionAcceptor; @@ -51,8 +53,7 @@ public static LocalClientTransport create(String name) { return new LocalClientTransport(name); } - @Override - public Mono connect() { + private Mono connect() { return Mono.defer( () -> { ServerDuplexConnectionAcceptor server = LocalServerTransport.findServer(name); @@ -69,4 +70,17 @@ public Mono connect() { return Mono.just((DuplexConnection) new LocalDuplexConnection(in, out, closeNotifier)); }); } + + @Override + public Mono connect(int mtu) { + Mono connect = connect(); + if (mtu > 0) { + return connect.map( + duplexConnection -> + new FragmentationDuplexConnection( + duplexConnection, ByteBufAllocator.DEFAULT, mtu, false)); + } else { + return connect; + } + } } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java index 68e7d462f..c1850b81c 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java @@ -16,8 +16,10 @@ package io.rsocket.transport.local; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.Closeable; import io.rsocket.DuplexConnection; +import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import java.util.Objects; @@ -76,6 +78,20 @@ public static void dispose(String name) { registry.remove(name); } + /** + * Retrieves an instance of {@link ServerDuplexConnectionAcceptor} based on the name of its {@code + * LocalServerTransport}. Returns {@code null} if that server is not registered. + * + * @param name the name of the server to retrieve + * @return the server if it has been registered, {@code null} otherwise + * @throws NullPointerException if {@code name} is {@code null} + */ + static @Nullable ServerDuplexConnectionAcceptor findServer(String name) { + Objects.requireNonNull(name, "name must not be null"); + + return registry.get(name); + } + /** * Returns a new {@link LocalClientTransport} that is connected to this {@code * LocalServerTransport}. @@ -88,13 +104,13 @@ public LocalClientTransport clientTransport() { } @Override - public Mono start(ConnectionAcceptor acceptor) { + public Mono start(ConnectionAcceptor acceptor, int mtu) { Objects.requireNonNull(acceptor, "acceptor must not be null"); return Mono.create( sink -> { ServerDuplexConnectionAcceptor serverDuplexConnectionAcceptor = - new ServerDuplexConnectionAcceptor(name, acceptor); + new ServerDuplexConnectionAcceptor(name, acceptor, mtu); if (registry.putIfAbsent(name, serverDuplexConnectionAcceptor) != null) { throw new IllegalStateException("name already registered: " + name); @@ -104,20 +120,6 @@ public Mono start(ConnectionAcceptor acceptor) { }); } - /** - * Retrieves an instance of {@link ServerDuplexConnectionAcceptor} based on the name of its {@code - * LocalServerTransport}. Returns {@code null} if that server is not registered. - * - * @param name the name of the server to retrieve - * @return the server if it has been registered, {@code null} otherwise - * @throws NullPointerException if {@code name} is {@code null} - */ - static @Nullable ServerDuplexConnectionAcceptor findServer(String name) { - Objects.requireNonNull(name, "name must not be null"); - - return registry.get(name); - } - /** * Returns the name of this instance. * @@ -138,6 +140,8 @@ static class ServerDuplexConnectionAcceptor implements Consumer onClose = MonoProcessor.create(); + private final int mtu; + /** * Creates a new instance * @@ -145,17 +149,24 @@ static class ServerDuplexConnectionAcceptor implements Consumer 0) { + duplexConnection = + new FragmentationDuplexConnection( + duplexConnection, ByteBufAllocator.DEFAULT, mtu, false); + } + acceptor.apply(duplexConnection).subscribe(); } diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientTransportTest.java index 92478b0bd..4cfee9a01 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalClientTransportTest.java @@ -32,8 +32,8 @@ void connect() { LocalServerTransport serverTransport = LocalServerTransport.createEphemeral(); serverTransport - .start(duplexConnection -> Mono.empty()) - .flatMap(closeable -> LocalClientTransport.create(serverTransport.getName()).connect()) + .start(duplexConnection -> Mono.empty(), 0) + .flatMap(closeable -> LocalClientTransport.create(serverTransport.getName()).connect(0)) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -43,7 +43,7 @@ void connect() { @Test void connectNoServer() { LocalClientTransport.create("test-name") - .connect() + .connect(0) .as(StepVerifier::create) .verifyErrorMessage("Could not find server: test-name"); } diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalServerTransportTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalServerTransportTest.java index 7fb350432..1656ed08d 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalServerTransportTest.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalServerTransportTest.java @@ -63,7 +63,7 @@ void findServer() { LocalServerTransport serverTransport = LocalServerTransport.createEphemeral(); serverTransport - .start(duplexConnection -> Mono.empty()) + .start(duplexConnection -> Mono.empty(), 0) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -97,7 +97,7 @@ void named() { @Test void start() { LocalServerTransport.createEphemeral() - .start(duplexConnection -> Mono.empty()) + .start(duplexConnection -> Mono.empty(), 0) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -107,7 +107,7 @@ void start() { @Test void startNullAcceptor() { assertThatNullPointerException() - .isThrownBy(() -> LocalServerTransport.createEphemeral().start(null)) + .isThrownBy(() -> LocalServerTransport.createEphemeral().start(null, 0)) .withMessage("acceptor must not be null"); } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java index b84201ac9..7b33bcf94 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java @@ -88,22 +88,19 @@ private ChannelPromise writeCleanupPromise(V poll) { .newPromise() .addListener( future -> { - try { - if (requested != Long.MAX_VALUE) { - requested--; - } - requestedUpstream--; - pending--; - - InnerSubscriber is = (InnerSubscriber) INNER_SUBSCRIBER.get(SendPublisher.this); - if (is != null) { - is.tryRequestMoreUpstream(); - tryComplete(is); - } - } finally { - if (poll.refCnt() > 0) { - ReferenceCountUtil.safeRelease(poll); - } + if (requested != Long.MAX_VALUE) { + requested--; + } + requestedUpstream--; + pending--; + + InnerSubscriber is = (InnerSubscriber) INNER_SUBSCRIBER.get(SendPublisher.this); + if (is != null) { + is.tryRequestMoreUpstream(); + tryComplete(is); + } + if (poll.refCnt() > 0) { + ReferenceCountUtil.safeRelease(poll); } }); } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java index 57e3ff0a9..9b2e60d5c 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java @@ -35,12 +35,25 @@ public final class TcpDuplexConnection implements DuplexConnection { private final Connection connection; private final Disposable channelClosed; private final ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + private final boolean encodeLength; + /** * Creates a new instance * - * @param connection the {@link Connection} to for managing the server + * @param connection the {@link Connection} for managing the server */ public TcpDuplexConnection(Connection connection) { + this(connection, true); + } + + /** + * Creates a new instance + * + * @param encodeLength indicates if this connection should encode the length or not. + * @param connection the {@link Connection} to for managing the server + */ + public TcpDuplexConnection(Connection connection, boolean encodeLength) { + this.encodeLength = encodeLength; this.connection = Objects.requireNonNull(connection, "connection must not be null"); this.channelClosed = FutureMono.from(connection.channel().closeFuture()) @@ -77,15 +90,7 @@ public Mono onClose() { @Override public Flux receive() { - return connection - .inbound() - .receive() - .map( - byteBuf -> { - ByteBuf frame = FrameLengthFlyweight.frame(byteBuf); - frame.retain(); - return frame; - }); + return connection.inbound().receive().map(this::decode); } @Override @@ -101,20 +106,29 @@ public Mono send(Publisher frames) { queueSubscription, frameFlux, connection.channel(), - frame -> - FrameLengthFlyweight.encode(allocator, frame.readableBytes(), frame) - .retain(), + this::encode, ByteBuf::readableBytes); } else { return new SendPublisher<>( - frameFlux, - connection.channel(), - frame -> - FrameLengthFlyweight.encode(allocator, frame.readableBytes(), frame) - .retain(), - ByteBuf::readableBytes); + frameFlux, connection.channel(), this::encode, ByteBuf::readableBytes); } }) .then(); } + + private ByteBuf encode(ByteBuf frame) { + if (encodeLength) { + return FrameLengthFlyweight.encode(allocator, frame.readableBytes(), frame).retain(); + } else { + return frame; + } + } + + private ByteBuf decode(ByteBuf frame) { + if (encodeLength) { + return FrameLengthFlyweight.frame(frame).retain(); + } else { + return frame; + } + } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java index 291494f3b..7c1070317 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java @@ -16,7 +16,9 @@ package io.rsocket.transport.netty.client; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; +import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.RSocketLengthCodec; @@ -91,10 +93,18 @@ public static TcpClientTransport create(TcpClient client) { } @Override - public Mono connect() { + public Mono connect(int mtu) { return client .doOnConnected(c -> c.addHandlerLast(new RSocketLengthCodec())) .connect() - .map(TcpDuplexConnection::new); + .map( + c -> { + if (mtu > 0) { + return new FragmentationDuplexConnection( + new TcpDuplexConnection(c, false), ByteBufAllocator.DEFAULT, mtu, true); + } else { + return new TcpDuplexConnection(c); + } + }); } } 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 111a37e98..99de91d41 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 @@ -19,7 +19,9 @@ import static io.rsocket.transport.netty.UriUtils.getPort; import static io.rsocket.transport.netty.UriUtils.isSecure; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; +import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.TransportHeaderAware; @@ -133,14 +135,31 @@ public static WebsocketClientTransport create(HttpClient client, String path) { return new WebsocketClientTransport(client, path); } + private static TcpClient createClient(URI uri) { + if (isSecure(uri)) { + return TcpClient.create().secure().host(uri.getHost()).port(getPort(uri, 443)); + } else { + return TcpClient.create().host(uri.getHost()).port(getPort(uri, 80)); + } + } + @Override - public Mono connect() { + public Mono connect(int mtu) { return client .headers(headers -> transportHeaders.get().forEach(headers::set)) .websocket() .uri(path) .connect() - .map(WebsocketDuplexConnection::new); + .map( + c -> { + DuplexConnection connection = new WebsocketDuplexConnection(c); + if (mtu > 0) { + connection = + new FragmentationDuplexConnection( + connection, ByteBufAllocator.DEFAULT, mtu, false); + } + return connection; + }); } @Override @@ -148,12 +167,4 @@ public void setTransportHeaders(Supplier> transportHeaders) this.transportHeaders = Objects.requireNonNull(transportHeaders, "transportHeaders must not be null"); } - - private static TcpClient createClient(URI uri) { - if (isSecure(uri)) { - return TcpClient.create().secure().host(uri.getHost()).port(getPort(uri, 443)); - } else { - return TcpClient.create().host(uri.getHost()).port(getPort(uri, 80)); - } - } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java index 6965499a8..11adae8a6 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java @@ -16,6 +16,9 @@ package io.rsocket.transport.netty.server; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.RSocketLengthCodec; @@ -89,14 +92,21 @@ public static TcpServerTransport create(TcpServer server) { } @Override - public Mono start(ConnectionAcceptor acceptor) { + public Mono start(ConnectionAcceptor acceptor, int mtu) { Objects.requireNonNull(acceptor, "acceptor must not be null"); return server .doOnConnection( c -> { c.addHandlerLast(new RSocketLengthCodec()); - TcpDuplexConnection connection = new TcpDuplexConnection(c); + DuplexConnection connection; + if (mtu > 0) { + connection = + new FragmentationDuplexConnection( + new TcpDuplexConnection(c, false), ByteBufAllocator.DEFAULT, mtu, true); + } else { + connection = new TcpDuplexConnection(c); + } acceptor.apply(connection).then(Mono.never()).subscribe(c.disposeSubscriber()); }) .bind() diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java index b9bb43e6e..fc2ab28bb 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java @@ -16,19 +16,18 @@ package io.rsocket.transport.netty.server; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.Closeable; +import io.rsocket.DuplexConnection; +import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.WebsocketDuplexConnection; import java.util.Objects; -import java.util.function.BiFunction; import java.util.function.Consumer; -import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import reactor.netty.Connection; import reactor.netty.http.server.HttpServer; import reactor.netty.http.server.HttpServerRoutes; -import reactor.netty.http.websocket.WebsocketInbound; -import reactor.netty.http.websocket.WebsocketOutbound; /** * An implementation of {@link ServerTransport} that connects via Websocket and listens on specified @@ -58,34 +57,26 @@ public WebsocketRouteTransport( } @Override - public Mono start(ConnectionAcceptor acceptor) { + public Mono start(ConnectionAcceptor acceptor, int mtu) { Objects.requireNonNull(acceptor, "acceptor must not be null"); return server .route( routes -> { routesBuilder.accept(routes); - routes.ws(path, newHandler(acceptor)); + routes.ws( + path, + (in, out) -> { + DuplexConnection connection = new WebsocketDuplexConnection((Connection) in); + if (mtu > 0) { + connection = + new FragmentationDuplexConnection( + connection, ByteBufAllocator.DEFAULT, mtu, false); + } + return acceptor.apply(connection).then(out.neverComplete()); + }); }) .bind() .map(CloseableChannel::new); } - - /** - * Creates a new Websocket handler - * - * @param acceptor the {@link ConnectionAcceptor} to use with the handler - * @return a new Websocket handler - * @throws NullPointerException if {@code acceptor} is {@code null} - */ - static BiFunction> newHandler( - ConnectionAcceptor acceptor) { - - Objects.requireNonNull(acceptor, "acceptor must not be null"); - - return (in, out) -> { - WebsocketDuplexConnection connection = new WebsocketDuplexConnection((Connection) in); - return acceptor.apply(connection).then(out.neverComplete()); - }; - } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java index b6ef5eaea..9e1af2395 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java @@ -16,15 +16,20 @@ package io.rsocket.transport.netty.server; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.TransportHeaderAware; +import io.rsocket.transport.netty.WebsocketDuplexConnection; import java.net.InetSocketAddress; import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.function.Supplier; import reactor.core.publisher.Mono; +import reactor.netty.Connection; import reactor.netty.http.server.HttpServer; /** @@ -101,14 +106,23 @@ public void setTransportHeaders(Supplier> transportHeaders) } @Override - public Mono start(ConnectionAcceptor acceptor) { + public Mono start(ConnectionAcceptor acceptor, int mtu) { Objects.requireNonNull(acceptor, "acceptor must not be null"); return server .handle( (request, response) -> { transportHeaders.get().forEach(response::addHeader); - return response.sendWebsocket(WebsocketRouteTransport.newHandler(acceptor)); + return response.sendWebsocket( + (in, out) -> { + DuplexConnection connection = new WebsocketDuplexConnection((Connection) in); + if (mtu > 0) { + connection = + new FragmentationDuplexConnection( + connection, ByteBufAllocator.DEFAULT, mtu, false); + } + return acceptor.apply(connection).then(out.neverComplete()); + }); }) .bind() .map(CloseableChannel::new); diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/TcpClientTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/TcpClientTransportTest.java index 388001fb6..e0bdb9cd7 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/TcpClientTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/TcpClientTransportTest.java @@ -37,8 +37,8 @@ void connect() { TcpServerTransport serverTransport = TcpServerTransport.create(address); serverTransport - .start(duplexConnection -> Mono.empty()) - .flatMap(context -> TcpClientTransport.create(context.address()).connect()) + .start(duplexConnection -> Mono.empty(), 0) + .flatMap(context -> TcpClientTransport.create(context.address()).connect(0)) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -47,7 +47,7 @@ void connect() { @DisplayName("create generates error if server not started") @Test void connectNoServer() { - TcpClientTransport.create(8000).connect().as(StepVerifier::create).verifyError(); + TcpClientTransport.create(8000).connect(0).as(StepVerifier::create).verifyError(); } @DisplayName("creates client with BindAddress") diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java index 202c5b3f3..58a8776a4 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java @@ -39,8 +39,8 @@ void connect() { WebsocketServerTransport serverTransport = WebsocketServerTransport.create(address); serverTransport - .start(duplexConnection -> Mono.empty()) - .flatMap(context -> WebsocketClientTransport.create(context.address()).connect()) + .start(duplexConnection -> Mono.empty(), 0) + .flatMap(context -> WebsocketClientTransport.create(context.address()).connect(0)) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -49,7 +49,7 @@ void connect() { @DisplayName("create generates error if server not started") @Test void connectNoServer() { - WebsocketClientTransport.create(8000).connect().as(StepVerifier::create).verifyError(); + WebsocketClientTransport.create(8000).connect(0).as(StepVerifier::create).verifyError(); } @DisplayName("creates client with BindAddress") diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/TcpServerTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/TcpServerTransportTest.java index 15a216b96..84c185e26 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/TcpServerTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/TcpServerTransportTest.java @@ -87,7 +87,7 @@ void start() { TcpServerTransport serverTransport = TcpServerTransport.create(address); serverTransport - .start(duplexConnection -> Mono.empty()) + .start(duplexConnection -> Mono.empty(), 0) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -97,7 +97,7 @@ void start() { @Test void startNullAcceptor() { assertThatNullPointerException() - .isThrownBy(() -> TcpServerTransport.create(8000).start(null)) + .isThrownBy(() -> TcpServerTransport.create(8000).start(null, 0)) .withMessage("acceptor must not be null"); } } diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java index 66822890a..e94bef13c 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java @@ -16,7 +16,6 @@ package io.rsocket.transport.netty.server; -import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import org.junit.jupiter.api.DisplayName; @@ -57,20 +56,6 @@ void constructorNullServer() { .withMessage("server must not be null"); } - @DisplayName("creates a new handler") - @Test - void newHandler() { - assertThat(WebsocketRouteTransport.newHandler(duplexConnection -> null)).isNotNull(); - } - - @DisplayName("newHandler throws NullPointerException with null acceptor") - @Test - void newHandlerNullAcceptor() { - assertThatNullPointerException() - .isThrownBy(() -> WebsocketRouteTransport.newHandler(null)) - .withMessage("acceptor must not be null"); - } - @DisplayName("starts server") @Test void start() { @@ -78,7 +63,7 @@ void start() { new WebsocketRouteTransport(HttpServer.create(), routes -> {}, "/test-path"); serverTransport - .start(duplexConnection -> Mono.empty()) + .start(duplexConnection -> Mono.empty(), 0) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -91,7 +76,7 @@ void startNullAcceptor() { .isThrownBy( () -> new WebsocketRouteTransport(HttpServer.create(), routes -> {}, "/test-path") - .start(null)) + .start(null, 0)) .withMessage("acceptor must not be null"); } } diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java index d1a6b374e..7a5e360d2 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java @@ -102,7 +102,7 @@ void start() { WebsocketServerTransport serverTransport = WebsocketServerTransport.create(address); serverTransport - .start(duplexConnection -> Mono.empty()) + .start(duplexConnection -> Mono.empty(), 0) .as(StepVerifier::create) .expectNextCount(1) .verifyComplete(); @@ -112,7 +112,7 @@ void start() { @Test void startNullAcceptor() { assertThatNullPointerException() - .isThrownBy(() -> WebsocketServerTransport.create(8000).start(null)) + .isThrownBy(() -> WebsocketServerTransport.create(8000).start(null, 0)) .withMessage("acceptor must not be null"); } } diff --git a/rsocket-transport-netty/src/test/resources/logback-test.xml b/rsocket-transport-netty/src/test/resources/logback-test.xml index 49b11d6fb..7150e3f0f 100644 --- a/rsocket-transport-netty/src/test/resources/logback-test.xml +++ b/rsocket-transport-netty/src/test/resources/logback-test.xml @@ -24,6 +24,7 @@ + From 5e33ed800375c62f69df43665359f2dc951dee62 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Wed, 27 Feb 2019 11:22:35 -0800 Subject: [PATCH 003/181] Revert "add [openjdk11] to Travis build matrix (#586)" (#587) This reverts commit 05a99a1c96c7a0ab5fb100cbf924efc7d808ce69. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3871823c4..2743ae0bc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,7 @@ language: java jdk: - oraclejdk8 -- openjdk11 +# - oraclejdk9 env: global: From 6efab27a1ef0ef45dfff4146031acaea8c7c6082 Mon Sep 17 00:00:00 2001 From: Ryland Degnan Date: Wed, 27 Feb 2019 11:41:13 -0800 Subject: [PATCH 004/181] Update version to 0.12.1-RC1 --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9cd597b08..c2e6e8a22 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,4 +12,4 @@ # limitations under the License. # -version=0.12.1-SNAPSHOT +version=0.12.1-RC1 From 2ce11a018ccaf5c42f9230da1e475f65ebf144e0 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 28 Feb 2019 07:53:23 +0200 Subject: [PATCH 005/181] Bugfix/#589 (#590) * provides throwing test * provides bug fix with path Signed-off-by: Oleh Dokuka --- .../client/WebsocketClientTransport.java | 6 +++- .../client/WebsocketClientTransportTest.java | 31 ++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) 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 99de91d41..fa364ef53 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 @@ -42,6 +42,8 @@ */ public final class WebsocketClientTransport implements ClientTransport, TransportHeaderAware { + private static final String DEFAULT_PATH = "/"; + private final HttpClient client; private String path; @@ -117,7 +119,7 @@ public static WebsocketClientTransport create(URI uri) { public static WebsocketClientTransport create(TcpClient client) { Objects.requireNonNull(client, "client must not be null"); - return create(HttpClient.from(client), "/"); + return create(HttpClient.from(client), DEFAULT_PATH); } /** @@ -132,6 +134,8 @@ public static WebsocketClientTransport create(HttpClient client, String path) { Objects.requireNonNull(client, "client must not be null"); Objects.requireNonNull(path, "path must not be null"); + path = path.startsWith(DEFAULT_PATH) ? path : (DEFAULT_PATH + path); + return new WebsocketClientTransport(client, path); } diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java index 58a8776a4..b2aba0235 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java @@ -55,13 +55,25 @@ void connectNoServer() { @DisplayName("creates client with BindAddress") @Test void createBindAddress() { - assertThat(WebsocketClientTransport.create("test-bind-address", 8000)).isNotNull(); + assertThat(WebsocketClientTransport.create("test-bind-address", 8000)) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/"); } @DisplayName("creates client with HttpClient") @Test void createHttpClient() { - assertThat(WebsocketClientTransport.create(HttpClient.create(), "/")).isNotNull(); + assertThat(WebsocketClientTransport.create(HttpClient.create(), "/")) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/"); + } + + @DisplayName("creates client with HttpClient and path without root") + @Test + void createHttpClientWithPathWithoutRoot() { + assertThat(WebsocketClientTransport.create(HttpClient.create(), "test")) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/test"); } @DisplayName("creates client with InetSocketAddress") @@ -70,7 +82,8 @@ void createInetSocketAddress() { assertThat( WebsocketClientTransport.create( InetSocketAddress.createUnresolved("test-bind-address", 8000))) - .isNotNull(); + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/"); } @DisplayName("create throws NullPointerException with null bindAddress") @@ -122,7 +135,17 @@ void createPort() { @DisplayName("creates client with URI") @Test void createUri() { - assertThat(WebsocketClientTransport.create(URI.create("ws://test-host/"))).isNotNull(); + assertThat(WebsocketClientTransport.create(URI.create("ws://test-host"))) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/"); + } + + @DisplayName("creates client with URI path") + @Test + void createUriPath() { + assertThat(WebsocketClientTransport.create(URI.create("ws://test-host/test"))) + .isNotNull() + .hasFieldOrPropertyWithValue("path", "/test"); } @DisplayName("sets transport headers") From 058878bba3fd1b1e97001539fc809d46a04cc07c Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Thu, 28 Feb 2019 11:22:38 -0800 Subject: [PATCH 006/181] added missing retain on metadata when reassembling frames Signed-off-by: Robert Roeser --- gradle.properties | 2 +- .../fragmentation/FrameReassembler.java | 2 +- .../io/rsocket/integration/FragmentTest.java | 171 ++++++++++++++++++ 3 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java diff --git a/gradle.properties b/gradle.properties index c2e6e8a22..98261c8fc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,4 +12,4 @@ # limitations under the License. # -version=0.12.1-RC1 +version=0.12.1-RC2 diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java index 1d0ae6792..32815779a 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java @@ -267,7 +267,7 @@ private ByteBuf assembleFrameWithMetadata(ByteBuf frame, int streamId, ByteBuf h ByteBuf data = assembleData(frame, streamId); - return FragmentationFlyweight.encode(allocator, header, metadata, data); + return FragmentationFlyweight.encode(allocator, header, metadata.retain(), data); } private ByteBuf assembleData(ByteBuf frame, int streamId) { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java new file mode 100644 index 000000000..61a6d0f20 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java @@ -0,0 +1,171 @@ +/* + * Copyright 2015-2018 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.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.rsocket.AbstractRSocket; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.RSocketFactory; +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 io.rsocket.util.RSocketProxy; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class FragmentTest { + private static final int frameSize = 128; + private AbstractRSocket handler; + private CloseableChannel server; + private String message = null; + private String metaData = null; + + @BeforeEach + public void startup() { + int randomPort = ThreadLocalRandom.current().nextInt(10_000, 20_000); + StringBuilder message = new StringBuilder(); + StringBuilder metaData = new StringBuilder(); + for (int i = 0; i < 100; i++) { + message.append("RESPONSE "); + metaData.append("METADATA "); + } + this.message = message.toString(); + this.metaData = metaData.toString(); + TcpServerTransport serverTransport = TcpServerTransport.create(randomPort); + server = + RSocketFactory.receive() + .fragment(frameSize) + .acceptor((setup, sendingSocket) -> Mono.just(new RSocketProxy(handler))) + .transport(serverTransport) + .start() + .block(); + } + + private RSocket buildClient() { + return RSocketFactory.connect() + .fragment(frameSize) + .transport(TcpClientTransport.create(server.address())) + .start() + .block(); + } + + @AfterEach + public void cleanup() { + server.dispose(); + } + + @Test + void testFragmentNoMetaData() { + System.out.println( + "-------------------------------------------------testFragmentNoMetaData-------------------------------------------------"); + handler = + new AbstractRSocket() { + @Override + public Flux requestStream(Payload payload) { + String request = payload.getDataUtf8(); + String metaData = payload.getMetadataUtf8(); + System.out.println("request message: " + request); + System.out.println("request metadata: " + metaData); + + return Flux.just(DefaultPayload.create(request)); + } + }; + + RSocket client = buildClient(); + + System.out.println("original message: " + message); + System.out.println("original metadata: " + metaData); + Payload payload = client.requestStream(DefaultPayload.create(message)).blockLast(); + System.out.println("response message: " + payload.getDataUtf8()); + System.out.println("response metadata: " + payload.getMetadataUtf8()); + + assertThat(message).isEqualTo(payload.getDataUtf8()); + } + + @Test + void testFragmentRequestMetaDataOnly() { + System.out.println( + "-------------------------------------------------testFragmentRequestMetaDataOnly-------------------------------------------------"); + handler = + new AbstractRSocket() { + @Override + public Flux requestStream(Payload payload) { + String request = payload.getDataUtf8(); + String metaData = payload.getMetadataUtf8(); + System.out.println("request message: " + request); + System.out.println("request metadata: " + metaData); + + return Flux.just(DefaultPayload.create(request)); + } + }; + + RSocket client = buildClient(); + + System.out.println("original message: " + message); + System.out.println("original metadata: " + metaData); + Payload payload = client.requestStream(DefaultPayload.create(message, metaData)).blockLast(); + System.out.println("response message: " + payload.getDataUtf8()); + System.out.println("response metadata: " + payload.getMetadataUtf8()); + + assertThat(message).isEqualTo(payload.getDataUtf8()); + } + + @Test + void testFragmentBothMetaData() { + System.out.println( + "-------------------------------------------------testFragmentBothMetaData-------------------------------------------------"); + handler = + new AbstractRSocket() { + @Override + public Flux requestStream(Payload payload) { + String request = payload.getDataUtf8(); + String metaData = payload.getMetadataUtf8(); + System.out.println("request message: " + request); + System.out.println("request metadata: " + metaData); + + return Flux.just(DefaultPayload.create(request, metaData)); + } + + @Override + public Mono requestResponse(Payload payload) { + String request = payload.getDataUtf8(); + String metaData = payload.getMetadataUtf8(); + System.out.println("request message: " + request); + System.out.println("request metadata: " + metaData); + + return Mono.just(DefaultPayload.create(request, metaData)); + } + }; + + RSocket client = buildClient(); + + System.out.println("original message: " + message); + System.out.println("original metadata: " + metaData); + Payload payload = client.requestStream(DefaultPayload.create(message, metaData)).blockLast(); + System.out.println("response message: " + payload.getDataUtf8()); + System.out.println("response metadata: " + payload.getMetadataUtf8()); + + assertThat(message).isEqualTo(payload.getDataUtf8()); + } +} From f60fbe664a85f0fd6704a26027222582c8cd1fa4 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Thu, 28 Feb 2019 11:46:23 -0800 Subject: [PATCH 007/181] update version Signed-off-by: Robert Roeser --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 98261c8fc..f47d03e7e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,4 +12,4 @@ # limitations under the License. # -version=0.12.1-RC2 +version=0.12.1-RC2-SNAPSHOT From 82ef2e118c460c9f81862663c68c92305e672e83 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Thu, 28 Feb 2019 11:57:16 -0800 Subject: [PATCH 008/181] added missing retain on metadata when reassembling frames (#591) * added missing retain on metadata when reassembling frames * update version Signed-off-by: Robert Roeser --- gradle.properties | 2 +- .../fragmentation/FrameReassembler.java | 2 +- .../io/rsocket/integration/FragmentTest.java | 171 ++++++++++++++++++ 3 files changed, 173 insertions(+), 2 deletions(-) create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java diff --git a/gradle.properties b/gradle.properties index c2e6e8a22..f47d03e7e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,4 +12,4 @@ # limitations under the License. # -version=0.12.1-RC1 +version=0.12.1-RC2-SNAPSHOT diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java index 1d0ae6792..32815779a 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java @@ -267,7 +267,7 @@ private ByteBuf assembleFrameWithMetadata(ByteBuf frame, int streamId, ByteBuf h ByteBuf data = assembleData(frame, streamId); - return FragmentationFlyweight.encode(allocator, header, metadata, data); + return FragmentationFlyweight.encode(allocator, header, metadata.retain(), data); } private ByteBuf assembleData(ByteBuf frame, int streamId) { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java new file mode 100644 index 000000000..61a6d0f20 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java @@ -0,0 +1,171 @@ +/* + * Copyright 2015-2018 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.integration; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.rsocket.AbstractRSocket; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.RSocketFactory; +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 io.rsocket.util.RSocketProxy; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class FragmentTest { + private static final int frameSize = 128; + private AbstractRSocket handler; + private CloseableChannel server; + private String message = null; + private String metaData = null; + + @BeforeEach + public void startup() { + int randomPort = ThreadLocalRandom.current().nextInt(10_000, 20_000); + StringBuilder message = new StringBuilder(); + StringBuilder metaData = new StringBuilder(); + for (int i = 0; i < 100; i++) { + message.append("RESPONSE "); + metaData.append("METADATA "); + } + this.message = message.toString(); + this.metaData = metaData.toString(); + TcpServerTransport serverTransport = TcpServerTransport.create(randomPort); + server = + RSocketFactory.receive() + .fragment(frameSize) + .acceptor((setup, sendingSocket) -> Mono.just(new RSocketProxy(handler))) + .transport(serverTransport) + .start() + .block(); + } + + private RSocket buildClient() { + return RSocketFactory.connect() + .fragment(frameSize) + .transport(TcpClientTransport.create(server.address())) + .start() + .block(); + } + + @AfterEach + public void cleanup() { + server.dispose(); + } + + @Test + void testFragmentNoMetaData() { + System.out.println( + "-------------------------------------------------testFragmentNoMetaData-------------------------------------------------"); + handler = + new AbstractRSocket() { + @Override + public Flux requestStream(Payload payload) { + String request = payload.getDataUtf8(); + String metaData = payload.getMetadataUtf8(); + System.out.println("request message: " + request); + System.out.println("request metadata: " + metaData); + + return Flux.just(DefaultPayload.create(request)); + } + }; + + RSocket client = buildClient(); + + System.out.println("original message: " + message); + System.out.println("original metadata: " + metaData); + Payload payload = client.requestStream(DefaultPayload.create(message)).blockLast(); + System.out.println("response message: " + payload.getDataUtf8()); + System.out.println("response metadata: " + payload.getMetadataUtf8()); + + assertThat(message).isEqualTo(payload.getDataUtf8()); + } + + @Test + void testFragmentRequestMetaDataOnly() { + System.out.println( + "-------------------------------------------------testFragmentRequestMetaDataOnly-------------------------------------------------"); + handler = + new AbstractRSocket() { + @Override + public Flux requestStream(Payload payload) { + String request = payload.getDataUtf8(); + String metaData = payload.getMetadataUtf8(); + System.out.println("request message: " + request); + System.out.println("request metadata: " + metaData); + + return Flux.just(DefaultPayload.create(request)); + } + }; + + RSocket client = buildClient(); + + System.out.println("original message: " + message); + System.out.println("original metadata: " + metaData); + Payload payload = client.requestStream(DefaultPayload.create(message, metaData)).blockLast(); + System.out.println("response message: " + payload.getDataUtf8()); + System.out.println("response metadata: " + payload.getMetadataUtf8()); + + assertThat(message).isEqualTo(payload.getDataUtf8()); + } + + @Test + void testFragmentBothMetaData() { + System.out.println( + "-------------------------------------------------testFragmentBothMetaData-------------------------------------------------"); + handler = + new AbstractRSocket() { + @Override + public Flux requestStream(Payload payload) { + String request = payload.getDataUtf8(); + String metaData = payload.getMetadataUtf8(); + System.out.println("request message: " + request); + System.out.println("request metadata: " + metaData); + + return Flux.just(DefaultPayload.create(request, metaData)); + } + + @Override + public Mono requestResponse(Payload payload) { + String request = payload.getDataUtf8(); + String metaData = payload.getMetadataUtf8(); + System.out.println("request message: " + request); + System.out.println("request metadata: " + metaData); + + return Mono.just(DefaultPayload.create(request, metaData)); + } + }; + + RSocket client = buildClient(); + + System.out.println("original message: " + message); + System.out.println("original metadata: " + metaData); + Payload payload = client.requestStream(DefaultPayload.create(message, metaData)).blockLast(); + System.out.println("response message: " + payload.getDataUtf8()); + System.out.println("response metadata: " + payload.getMetadataUtf8()); + + assertThat(message).isEqualTo(payload.getDataUtf8()); + } +} From aadcece31f80dbe065cca5436f100db42c034762 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Thu, 28 Feb 2019 12:02:03 -0800 Subject: [PATCH 009/181] release rc2 Signed-off-by: Robert Roeser --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f47d03e7e..98261c8fc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,4 +12,4 @@ # limitations under the License. # -version=0.12.1-RC2-SNAPSHOT +version=0.12.1-RC2 From d2daacc7bd7ae032b9fc968ca8b1309a17b5a56e Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Thu, 28 Feb 2019 12:56:05 -0800 Subject: [PATCH 010/181] added back request channel optimization Signed-off-by: Robert Roeser --- .../main/java/io/rsocket/RSocketServer.java | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java index a184e11a1..8626f2a13 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java @@ -41,10 +41,11 @@ import reactor.core.publisher.UnicastProcessor; /** Server side RSocket. Receives {@link ByteBuf}s from a {@link RSocketClient} */ -class RSocketServer implements RSocket { +class RSocketServer implements ResponderRSocket { private final DuplexConnection connection; private final RSocket requestHandler; + private final ResponderRSocket responderRSocket; private final PayloadDecoder payloadDecoder; private final Consumer errorConsumer; @@ -76,7 +77,11 @@ class RSocketServer implements RSocket { long ackTimeout) { this.allocator = allocator; this.connection = connection; + this.requestHandler = requestHandler; + this.responderRSocket = + (requestHandler instanceof ResponderRSocket) ? (ResponderRSocket) requestHandler : null; + this.payloadDecoder = payloadDecoder; this.errorConsumer = errorConsumer; this.sendingSubscriptions = Collections.synchronizedMap(new IntObjectHashMap<>()); @@ -209,6 +214,15 @@ public Flux requestChannel(Publisher payloads) { } } + @Override + public Flux requestChannel(Payload payload, Publisher payloads) { + try { + return responderRSocket.requestChannel(payload, payloads); + } catch (Throwable t) { + return Flux.error(t); + } + } + @Override public Mono metadataPush(Payload payload) { try { @@ -420,7 +434,11 @@ private void handleChannel(int streamId, Payload payload, int initialRequestN) { // and any later payload can be processed frames.onNext(payload); - handleStream(streamId, requestChannel(payloads), initialRequestN); + if (responderRSocket != null) { + handleStream(streamId, requestChannel(payload, payloads), initialRequestN); + } else { + handleStream(streamId, requestChannel(payloads), initialRequestN); + } } private void handleKeepAliveFrame(ByteBuf frame) { From e48658e28676b6dc6dcef20b2fb70b38f220ad8d Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 5 Mar 2019 13:41:04 +0200 Subject: [PATCH 011/181] fixes issue with requestChannel never completes if server cancels (#592) (#593) Signed-off-by: Oleh Dokuka --- .../main/java/io/rsocket/RSocketClient.java | 1 - .../java/io/rsocket/RSocketClientTest.java | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java index 902d8487e..64aa3ccbf 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java @@ -519,7 +519,6 @@ private void handleFrame(int streamId, FrameType type, ByteBuf frame) { case CANCEL: { LimitableRequestPublisher sender = senders.remove(streamId); - receivers.remove(streamId); if (sender != null) { sender.cancel(); } diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java index 2224ba393..788256c9e 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java @@ -35,6 +35,7 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; +import org.assertj.core.api.Assertions; import org.junit.Rule; import org.junit.Test; import org.reactivestreams.Publisher; @@ -44,6 +45,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.UnicastProcessor; public class RSocketClientTest { @@ -194,6 +196,26 @@ public void testChannelRequestCancellation() { .blockFirst(); } + @Test + public void testChannelRequestServerSideCancellation() { + MonoProcessor cancelled = MonoProcessor.create(); + UnicastProcessor request = UnicastProcessor.create(); + request.onNext(EmptyPayload.INSTANCE); + rule.socket.requestChannel(request).subscribe(cancelled); + int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); + rule.connection.addToReceivedBuffer( + CancelFrameFlyweight.encode(ByteBufAllocator.DEFAULT, streamId)); + rule.connection.addToReceivedBuffer( + PayloadFrameFlyweight.encodeComplete(ByteBufAllocator.DEFAULT, streamId)); + Flux.first( + cancelled, + Flux.error(new IllegalStateException("Channel request not cancelled")) + .delaySubscription(Duration.ofSeconds(1))) + .blockFirst(); + + Assertions.assertThat(request.isDisposed()).isTrue(); + } + public int sendRequestResponse(Publisher response) { Subscriber sub = TestSubscriber.create(); response.subscribe(sub); From a11ef3f487dcd68a69ca971ff09d54b96a0893ee Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 12 Mar 2019 21:59:44 +0200 Subject: [PATCH 012/181] fixes uncontrolled data sending in case of direct propagation of request from requester (#595) * fixes uncontrolled data sending in case of direct propagation of request from requester * fixes timeout typo * replaces forEach with explicit loop * optimize access to limitableRequestPublisher Signed-off-by: Oleh Dokuka --- .../main/java/io/rsocket/RSocketClient.java | 11 +- .../main/java/io/rsocket/RSocketServer.java | 53 ++++++++- .../internal/LimitableRequestPublisher.java | 40 ++++--- .../rsocket/internal/UnboundedProcessor.java | 4 + .../java/io/rsocket/RSocketClientTest.java | 29 +++++ .../java/io/rsocket/RSocketServerTest.java | 102 ++++++++++++++++++ .../integration/TcpIntegrationTest.java | 10 +- 7 files changed, 224 insertions(+), 25 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java index 64aa3ccbf..2a6becfce 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java @@ -98,7 +98,13 @@ class RSocketClient implements RSocket { connection.onClose().doFinally(signalType -> terminate()).subscribe(null, errorConsumer); connection - .send(sendProcessor) + .send( + sendProcessor.doOnRequest( + r -> { + for (LimitableRequestPublisher lrp : senders.values()) { + lrp.increaseInternalLimit(r); + } + })) .doFinally(this::handleSendProcessorCancel) .subscribe(null, this::handleSendProcessorError); @@ -335,7 +341,8 @@ private Flux handleChannel(Flux request) { .transform( f -> { LimitableRequestPublisher wrapped = - LimitableRequestPublisher.wrap(f); + LimitableRequestPublisher.wrap( + f, sendProcessor.available()); // Need to set this to one for first the frame wrapped.increaseRequestLimit(1); senders.put(streamId, wrapped); diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java index 8626f2a13..8a2fc1a60 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java @@ -49,6 +49,7 @@ class RSocketServer implements ResponderRSocket { private final PayloadDecoder payloadDecoder; private final Consumer errorConsumer; + private final Map sendingLimitableSubscriptions; private final Map sendingSubscriptions; private final Map> channelProcessors; @@ -84,6 +85,7 @@ class RSocketServer implements ResponderRSocket { this.payloadDecoder = payloadDecoder; this.errorConsumer = errorConsumer; + this.sendingLimitableSubscriptions = Collections.synchronizedMap(new IntObjectHashMap<>()); this.sendingSubscriptions = Collections.synchronizedMap(new IntObjectHashMap<>()); this.channelProcessors = Collections.synchronizedMap(new IntObjectHashMap<>()); @@ -92,7 +94,13 @@ class RSocketServer implements ResponderRSocket { this.sendProcessor = new UnboundedProcessor<>(); connection - .send(sendProcessor) + .send( + sendProcessor.doOnRequest( + r -> { + for (LimitableRequestPublisher lrp : sendingLimitableSubscriptions.values()) { + lrp.increaseInternalLimit(r); + } + })) .doFinally(this::handleSendProcessorCancel) .subscribe(null, this::handleSendProcessorError); @@ -138,6 +146,17 @@ private void handleSendProcessorError(Throwable t) { } }); + sendingLimitableSubscriptions + .values() + .forEach( + subscription -> { + try { + subscription.cancel(); + } catch (Throwable e) { + errorConsumer.accept(e); + } + }); + channelProcessors .values() .forEach( @@ -166,6 +185,17 @@ private void handleSendProcessorCancel(SignalType t) { } }); + sendingLimitableSubscriptions + .values() + .forEach( + subscription -> { + try { + subscription.cancel(); + } catch (Throwable e) { + errorConsumer.accept(e); + } + }); + channelProcessors .values() .forEach( @@ -261,6 +291,9 @@ private void cleanup() { private synchronized void cleanUpSendingSubscriptions() { sendingSubscriptions.values().forEach(Subscription::cancel); sendingSubscriptions.clear(); + + sendingLimitableSubscriptions.values().forEach(Subscription::cancel); + sendingLimitableSubscriptions.clear(); } private synchronized void cleanUpChannelProcessors() { @@ -391,12 +424,12 @@ private void handleStream(int streamId, Flux response, int initialReque .transform( frameFlux -> { LimitableRequestPublisher payloads = - LimitableRequestPublisher.wrap(frameFlux); - sendingSubscriptions.put(streamId, payloads); + LimitableRequestPublisher.wrap(frameFlux, sendProcessor.available()); + sendingLimitableSubscriptions.put(streamId, payloads); payloads.increaseRequestLimit(initialRequestN); return payloads; }) - .doFinally(signalType -> sendingSubscriptions.remove(streamId)) + .doFinally(signalType -> sendingLimitableSubscriptions.remove(streamId)) .subscribe( payload -> { ByteBuf byteBuf = null; @@ -449,6 +482,11 @@ private void handleKeepAliveFrame(ByteBuf frame) { private void handleCancelFrame(int streamId) { Subscription subscription = sendingSubscriptions.remove(streamId); + + if (subscription == null) { + subscription = sendingLimitableSubscriptions.get(streamId); + } + if (subscription != null) { subscription.cancel(); } @@ -460,7 +498,12 @@ private void handleError(int streamId, Throwable t) { } private void handleRequestN(int streamId, ByteBuf frame) { - final Subscription subscription = sendingSubscriptions.get(streamId); + Subscription subscription = sendingSubscriptions.get(streamId); + + if (subscription == null) { + subscription = sendingLimitableSubscriptions.get(streamId); + } + if (subscription != null) { int n = RequestNFrameFlyweight.requestN(frame); subscription.request(n >= Integer.MAX_VALUE ? Long.MAX_VALUE : n); diff --git a/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java b/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java index 17372ea01..d5a05375d 100755 --- a/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java @@ -31,6 +31,8 @@ public class LimitableRequestPublisher extends Flux implements Subscriptio private final AtomicBoolean canceled; + private final long prefetch; + private long internalRequested; private long externalRequested; @@ -39,13 +41,14 @@ public class LimitableRequestPublisher extends Flux implements Subscriptio private volatile @Nullable Subscription internalSubscription; - private LimitableRequestPublisher(Publisher source) { + private LimitableRequestPublisher(Publisher source, long prefetch) { this.source = source; + this.prefetch = prefetch; this.canceled = new AtomicBoolean(); } - public static LimitableRequestPublisher wrap(Publisher source) { - return new LimitableRequestPublisher<>(source); + public static LimitableRequestPublisher wrap(Publisher source, long prefetch) { + return new LimitableRequestPublisher<>(source, prefetch); } @Override @@ -60,6 +63,7 @@ public void subscribe(CoreSubscriber destination) { destination.onSubscribe(new InnerSubscription()); source.subscribe(new InnerSubscriber(destination)); + increaseInternalLimit(prefetch); } public void increaseRequestLimit(long n) { @@ -70,6 +74,14 @@ public void increaseRequestLimit(long n) { requestN(); } + public void increaseInternalLimit(long n) { + synchronized (this) { + internalRequested = Operators.addCap(n, internalRequested); + } + + requestN(); + } + @Override public void request(long n) { increaseRequestLimit(n); @@ -82,9 +94,17 @@ private void requestN() { return; } - r = Math.min(internalRequested, externalRequested); - externalRequested -= r; - internalRequested -= r; + if (externalRequested != Long.MAX_VALUE || internalRequested != Long.MAX_VALUE) { + r = Math.min(internalRequested, externalRequested); + if (externalRequested != Long.MAX_VALUE) { + externalRequested -= r; + } + if (internalRequested != Long.MAX_VALUE) { + internalRequested -= r; + } + } else { + r = Long.MAX_VALUE; + } } if (r > 0) { @@ -144,13 +164,7 @@ public void onComplete() { private class InnerSubscription implements Subscription { @Override - public void request(long n) { - synchronized (LimitableRequestPublisher.this) { - internalRequested = Operators.addCap(n, internalRequested); - } - - requestN(); - } + public void request(long n) {} @Override public void cancel() { 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 607a7ec73..88d486174 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -221,6 +221,10 @@ public void onSubscribe(Subscription s) { } } + public long available() { + return requested; + } + @Override public int getPrefetch() { return Integer.MAX_VALUE; diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java index 788256c9e..dd55b44aa 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java @@ -28,12 +28,15 @@ import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.*; +import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestSubscriber; import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; import java.time.Duration; import java.util.ArrayList; import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; import java.util.stream.Collectors; import org.assertj.core.api.Assertions; import org.junit.Rule; @@ -216,6 +219,32 @@ public void testChannelRequestServerSideCancellation() { Assertions.assertThat(request.isDisposed()).isTrue(); } + @Test(timeout = 2_000) + @SuppressWarnings("unchecked") + public void + testClientSideRequestChannelShouldNotHangInfinitelySendingElementsAndShouldProduceDataValuingConnectionBackpressure() { + final Queue requests = new ConcurrentLinkedQueue<>(); + rule.connection.dispose(); + rule.connection = new TestDuplexConnection(); + rule.connection.setInitialSendRequestN(256); + rule.init(); + + rule.socket + .requestChannel( + Flux.generate(s -> s.next(EmptyPayload.INSTANCE)).doOnRequest(requests::add)) + .subscribe(); + + int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); + + assertThat("Unexpected error.", rule.errors, is(empty())); + + rule.connection.addToReceivedBuffer( + RequestNFrameFlyweight.encode(ByteBufAllocator.DEFAULT, streamId, 2)); + rule.connection.addToReceivedBuffer( + RequestNFrameFlyweight.encode(ByteBufAllocator.DEFAULT, streamId, Integer.MAX_VALUE)); + Assertions.assertThat(requests).containsOnly(1L, 2L, 253L); + } + public int sendRequestResponse(Publisher response) { Subscriber sub = TestSubscriber.create(); response.subscribe(sub); diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java index 1d3417e02..9f2541975 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java @@ -29,12 +29,17 @@ import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; import java.util.Collection; +import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; +import org.assertj.core.api.Assertions; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; +import org.mockito.Mockito; +import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class RSocketServerTest { @@ -106,6 +111,89 @@ public Mono requestResponse(Payload payload) { assertThat("Subscription not cancelled.", cancelled.get(), is(true)); } + @Test(timeout = 2_000) + @SuppressWarnings("unchecked") + public void + testServerSideRequestStreamShouldNotHangInfinitelySendingElementsAndShouldProduceDataValuingConnectionBackpressure() { + final int streamId = 5; + final Queue received = new ConcurrentLinkedQueue<>(); + final Queue requests = new ConcurrentLinkedQueue<>(); + + rule.setAcceptingSocket( + new AbstractRSocket() { + @Override + public Flux requestStream(Payload payload) { + return Flux.generate(s -> s.next(payload.retain())).doOnRequest(requests::add); + } + }, + 256); + + rule.sendRequest(streamId, FrameType.REQUEST_STREAM); + + assertThat("Unexpected error.", rule.errors, is(empty())); + + Subscriber next = rule.connection.getSendSubscribers().iterator().next(); + + Mockito.doAnswer( + invocation -> { + received.add(invocation.getArgument(0)); + + if (received.size() == 256) { + throw new RuntimeException(); + } + + return null; + }) + .when(next) + .onNext(Mockito.any()); + + rule.connection.addToReceivedBuffer( + RequestNFrameFlyweight.encode(ByteBufAllocator.DEFAULT, streamId, Integer.MAX_VALUE)); + Assertions.assertThat(requests).containsOnly(1L, 2L, 253L); + } + + @Test(timeout = 2_000) + @SuppressWarnings("unchecked") + public void + testServerSideRequestChannelShouldNotHangInfinitelySendingElementsAndShouldProduceDataValuingConnectionBackpressure() { + final int streamId = 5; + final Queue received = new ConcurrentLinkedQueue<>(); + final Queue requests = new ConcurrentLinkedQueue<>(); + + rule.setAcceptingSocket( + new AbstractRSocket() { + @Override + public Flux requestChannel(Publisher payload) { + return Flux.generate(s -> s.next(EmptyPayload.INSTANCE)) + .doOnRequest(requests::add); + } + }, + 256); + + rule.sendRequest(streamId, FrameType.REQUEST_CHANNEL); + + assertThat("Unexpected error.", rule.errors, is(empty())); + + Subscriber next = rule.connection.getSendSubscribers().iterator().next(); + + Mockito.doAnswer( + invocation -> { + received.add(invocation.getArgument(0)); + + if (received.size() == 256) { + throw new RuntimeException(); + } + + return null; + }) + .when(next) + .onNext(Mockito.any()); + + rule.connection.addToReceivedBuffer( + RequestNFrameFlyweight.encode(ByteBufAllocator.DEFAULT, streamId, Integer.MAX_VALUE)); + Assertions.assertThat(requests).containsOnly(1L, 2L, 253L); + } + public static class ServerSocketRule extends AbstractSocketRule { private RSocket acceptingSocket; @@ -130,6 +218,15 @@ public void setAcceptingSocket(RSocket acceptingSocket) { super.init(); } + public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { + this.acceptingSocket = acceptingSocket; + connection = new TestDuplexConnection(); + connection.setInitialSendRequestN(prefetch); + connectSub = TestSubscriber.create(); + errors = new ConcurrentLinkedQueue<>(); + super.init(); + } + @Override protected RSocketServer newRSocket() { return new RSocketServer( @@ -144,6 +241,11 @@ private void sendRequest(int streamId, FrameType frameType) { ByteBuf request; switch (frameType) { + case REQUEST_CHANNEL: + request = + RequestChannelFrameFlyweight.encode( + ByteBufAllocator.DEFAULT, streamId, false, false, 1, EmptyPayload.INSTANCE); + break; case REQUEST_STREAM: request = RequestStreamFrameFlyweight.encode( diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java index f5d048508..41e437fee 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java @@ -67,7 +67,7 @@ public void cleanup() { server.dispose(); } - @Test(timeout = 5_000L) + @Test(timeout = 15_000L) public void testCompleteWithoutNext() { handler = new AbstractRSocket() { @@ -83,7 +83,7 @@ public Flux requestStream(Payload payload) { assertFalse(hasElements); } - @Test(timeout = 5_000L) + @Test(timeout = 15_000L) public void testSingleStream() { handler = new AbstractRSocket() { @@ -100,7 +100,7 @@ public Flux requestStream(Payload payload) { assertEquals("RESPONSE", result.getDataUtf8()); } - @Test(timeout = 5_000L) + @Test(timeout = 15_000L) public void testZeroPayload() { handler = new AbstractRSocket() { @@ -117,7 +117,7 @@ public Flux requestStream(Payload payload) { assertEquals("", result.getDataUtf8()); } - @Test(timeout = 5_000L) + @Test(timeout = 15_000L) public void testRequestResponseErrors() { handler = new AbstractRSocket() { @@ -151,7 +151,7 @@ public Mono requestResponse(Payload payload) { assertEquals("SUCCESS", response2.getDataUtf8()); } - @Test(timeout = 5_000L) + @Test(timeout = 15_000L) public void testTwoConcurrentStreams() throws InterruptedException { ConcurrentHashMap> map = new ConcurrentHashMap<>(); UnicastProcessor processor1 = UnicastProcessor.create(); From 576c0f525a6e8814d60007638ca29dcaac621682 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 14 Mar 2019 19:56:41 +0200 Subject: [PATCH 013/181] Resume support (#558) * Remove keep-alive related code from RSocket Client/Server, and implement keep-alive support as KeepAliveConnection: this change is due to RSocket resumption support implies sessions spanning multiple connections, and each connection liveness should be checked with keep-alives * RSocket resume support, with implementation of delivering last received positions by keep-alive frames * move ClientSetup & ServerSetup implementations from RSocketFactory * add test dependencies * use better queue for resumed frames cache limit frames cache size cleanup * external resume frames store * resume store saveFrames streaming support upstream frames requesting respects both downstream & resume save frames stream requests * Resumable connection: better distribution of downstream connection requestN among upstream subscribers * simplify UpstreamFrameSubscriber * fix concurrency issues * cleanup * apply formatter * reduce log level to debug * fix frame leak Signed-off-by: Maksym Ostroverkhov --- .../java/io/rsocket/KeepAliveHandler.java | 125 ------ .../main/java/io/rsocket/RSocketClient.java | 55 +-- .../main/java/io/rsocket/RSocketFactory.java | 350 +++++++++++----- .../main/java/io/rsocket/RSocketServer.java | 46 +-- .../main/java/io/rsocket/frame/FrameUtil.java | 2 +- .../io/rsocket/frame/ResumeFlyweight.java | 3 - .../rsocket/frame/ResumeFrameFlyweight.java | 101 +++++ .../io/rsocket/frame/ResumeOkFlyweight.java | 3 - .../rsocket/frame/ResumeOkFrameFlyweight.java | 22 + .../io/rsocket/frame/SetupFrameFlyweight.java | 48 ++- .../internal/ClientServerConnection.java | 40 ++ .../ClientServerInputMultiplexer.java | 71 +++- .../java/io/rsocket/internal/ClientSetup.java | 87 ++++ .../io/rsocket/internal/KeepAliveData.java | 41 ++ .../java/io/rsocket/internal/ServerSetup.java | 200 ++++++++++ .../rsocket/internal/UnboundedProcessor.java | 19 +- .../keepalive/KeepAliveConnection.java | 164 ++++++++ .../rsocket/keepalive/KeepAliveHandler.java | 169 ++++++++ .../plugins/DuplexConnectionInterceptor.java | 2 +- .../rsocket/resume/ClientRSocketSession.java | 166 ++++++++ .../java/io/rsocket/resume/ClientResume.java} | 28 +- .../resume/ClientResumeConfiguration.java | 54 +++ .../ExponentialBackoffResumeStrategy.java | 71 ++++ .../resume/InMemoryResumableFramesStore.java | 145 +++++++ .../resume/PeriodicResumeStrategy.java | 39 ++ .../io/rsocket/resume/RSocketSession.java | 50 +++ .../io/rsocket/resume/RequestListener.java | 32 ++ .../resume/ResumableDuplexConnection.java | 376 ++++++++++++++++++ .../rsocket/resume/ResumableFramesStore.java | 55 +++ .../rsocket/resume/ResumeAwareConnection.java | 25 ++ .../java/io/rsocket/resume/ResumeCache.java | 126 ------ .../resume/ResumeFramesSubscriber.java | 88 ++++ .../rsocket/resume/ResumeStateException.java | 36 ++ .../io/rsocket/resume/ResumeStateHolder.java | 22 + .../io/rsocket/resume/ResumeStrategy.java | 23 ++ .../java/io/rsocket/resume/ResumeToken.java | 41 +- .../java/io/rsocket/resume/ResumeUtil.java | 48 --- .../resume/ResumedFramesCalculator.java | 52 +++ .../io/rsocket/resume/ResumptionState.java | 67 ++++ .../rsocket/resume/ServerRSocketSession.java | 158 ++++++++ .../resume/ServerResumeConfiguration.java | 47 +++ .../io/rsocket/resume/SessionManager.java | 50 +++ .../resume/UpstreamFramesSubscriber.java | 190 +++++++++ ...PositionCounter.java => package-info.java} | 19 +- .../java/io/rsocket/util/ConnectionUtils.java | 17 + .../rsocket/util/DuplexConnectionProxy.java | 65 +++ .../main/java/io/rsocket/util/Function3.java | 22 + .../test/java/io/rsocket/KeepAliveTest.java | 306 ++++++++------ .../java/io/rsocket/RSocketClientTest.java | 10 +- .../java/io/rsocket/SetupRejectionTest.java | 1 - .../frame/ResumeFrameFlyweightTest.java | 39 ++ .../frame/ResumeOkFrameFlyweightTest.java | 16 + .../frame/SetupFrameFlyweightTest.java | 73 ++-- .../ClientServerInputMultiplexerTest.java | 2 +- .../io/rsocket/resume/ResumeCacheTest.java | 130 ------ .../rsocket/resume/ResumeCalculatorTest.java | 81 ++++ .../rsocket/resume/ResumeExpBackoffTest.java | 75 ++++ .../io/rsocket/resume/ResumeUtilTest.java | 51 --- rsocket-examples/build.gradle | 3 + .../resume/DisconnectableClientTransport.java | 75 ++++ .../rsocket/resume/ResumeIntegrationTest.java | 245 ++++++++++++ .../main/java/io/rsocket/test/TestFrames.java | 1 - .../transport/netty/SendPublisher.java | 11 +- 63 files changed, 3848 insertions(+), 931 deletions(-) delete mode 100644 rsocket-core/src/main/java/io/rsocket/KeepAliveHandler.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/frame/ResumeFlyweight.java create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameFlyweight.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFlyweight.java create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFrameFlyweight.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/ClientServerConnection.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/KeepAliveData.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java create mode 100644 rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveConnection.java create mode 100644 rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java rename rsocket-core/src/{test/java/io/rsocket/resume/ResumeTokenTest.java => main/java/io/rsocket/resume/ClientResume.java} (51%) create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ClientResumeConfiguration.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ExponentialBackoffResumeStrategy.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/PeriodicResumeStrategy.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumeAwareConnection.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumeCache.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumeFramesSubscriber.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumeStateException.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumeUtil.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumedFramesCalculator.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumptionState.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ServerResumeConfiguration.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java create mode 100644 rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java rename rsocket-core/src/main/java/io/rsocket/resume/{ResumePositionCounter.java => package-info.java} (61%) create mode 100644 rsocket-core/src/main/java/io/rsocket/util/ConnectionUtils.java create mode 100644 rsocket-core/src/main/java/io/rsocket/util/DuplexConnectionProxy.java create mode 100644 rsocket-core/src/main/java/io/rsocket/util/Function3.java create mode 100644 rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameFlyweightTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameFlyweightTest.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/resume/ResumeCacheTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/resume/ResumeExpBackoffTest.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/resume/ResumeUtilTest.java create mode 100644 rsocket-examples/src/test/java/io/rsocket/resume/DisconnectableClientTransport.java create mode 100644 rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/KeepAliveHandler.java b/rsocket-core/src/main/java/io/rsocket/KeepAliveHandler.java deleted file mode 100644 index 7eda01fbe..000000000 --- a/rsocket-core/src/main/java/io/rsocket/KeepAliveHandler.java +++ /dev/null @@ -1,125 +0,0 @@ -package io.rsocket; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.rsocket.frame.KeepAliveFrameFlyweight; -import java.time.Duration; -import reactor.core.Disposable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; -import reactor.core.publisher.UnicastProcessor; - -abstract class KeepAliveHandler implements Disposable { - private final KeepAlive keepAlive; - private final UnicastProcessor sent = UnicastProcessor.create(); - private final MonoProcessor timeout = MonoProcessor.create(); - private Disposable intervalDisposable; - private volatile long lastReceivedMillis; - - private KeepAliveHandler(KeepAlive keepAlive) { - this.keepAlive = keepAlive; - this.lastReceivedMillis = System.currentTimeMillis(); - this.intervalDisposable = - Flux.interval(Duration.ofMillis(keepAlive.getTickPeriod())) - .subscribe(v -> onIntervalTick()); - } - - static KeepAliveHandler ofServer(KeepAlive keepAlive) { - return new KeepAliveHandler.Server(keepAlive); - } - - static KeepAliveHandler ofClient(KeepAlive keepAlive) { - return new KeepAliveHandler.Client(keepAlive); - } - - @Override - public void dispose() { - sent.onComplete(); - timeout.onComplete(); - intervalDisposable.dispose(); - } - - public void receive(ByteBuf keepAliveFrame) { - this.lastReceivedMillis = System.currentTimeMillis(); - if (KeepAliveFrameFlyweight.respondFlag(keepAliveFrame)) { - doSend( - KeepAliveFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, - false, - 0, - KeepAliveFrameFlyweight.data(keepAliveFrame).retain())); - } - } - - public Flux send() { - return sent; - } - - public Mono timeout() { - return timeout; - } - - abstract void onIntervalTick(); - - void doSend(ByteBuf frame) { - sent.onNext(frame); - } - - void doCheckTimeout() { - long now = System.currentTimeMillis(); - if (now - lastReceivedMillis >= keepAlive.getTimeoutMillis()) { - timeout.onNext(keepAlive); - } - } - - private static class Server extends KeepAliveHandler { - - Server(KeepAlive keepAlive) { - super(keepAlive); - } - - @Override - void onIntervalTick() { - doCheckTimeout(); - } - } - - private static final class Client extends KeepAliveHandler { - - Client(KeepAlive keepAlive) { - super(keepAlive); - } - - @Override - void onIntervalTick() { - doCheckTimeout(); - doSend( - KeepAliveFrameFlyweight.encode(ByteBufAllocator.DEFAULT, true, 0, Unpooled.EMPTY_BUFFER)); - } - } - - static final class KeepAlive { - private final long tickPeriod; - private final long timeoutMillis; - - KeepAlive(Duration tickPeriod, Duration timeoutMillis, int maxTicks) { - this.tickPeriod = tickPeriod.toMillis(); - this.timeoutMillis = timeoutMillis.toMillis() + maxTicks * tickPeriod.toMillis(); - } - - KeepAlive(long tickPeriod, long timeoutMillis) { - this.tickPeriod = tickPeriod; - this.timeoutMillis = timeoutMillis; - } - - public long getTickPeriod() { - return tickPeriod; - } - - public long getTimeoutMillis() { - return timeoutMillis; - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java index 2a6becfce..debfc61a3 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java @@ -20,7 +20,6 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.util.ReferenceCountUtil; import io.netty.util.collection.IntObjectHashMap; -import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.exceptions.Exceptions; import io.rsocket.frame.*; import io.rsocket.frame.decoder.PayloadDecoder; @@ -28,7 +27,6 @@ import io.rsocket.internal.UnboundedProcessor; import io.rsocket.internal.UnicastMonoProcessor; import java.nio.channels.ClosedChannelException; -import java.time.Duration; import java.util.Collections; import java.util.Map; import java.util.concurrent.atomic.AtomicBoolean; @@ -54,25 +52,6 @@ class RSocketClient implements RSocket { private final UnboundedProcessor sendProcessor; private final Lifecycle lifecycle = new Lifecycle(); private final ByteBufAllocator allocator; - private KeepAliveHandler keepAliveHandler; - - /*server requester*/ - RSocketClient( - ByteBufAllocator allocator, - DuplexConnection connection, - PayloadDecoder payloadDecoder, - Consumer errorConsumer, - StreamIdSupplier streamIdSupplier) { - this( - allocator, - connection, - payloadDecoder, - errorConsumer, - streamIdSupplier, - Duration.ZERO, - Duration.ZERO, - 0); - } /*client requester*/ RSocketClient( @@ -80,10 +59,7 @@ class RSocketClient implements RSocket { DuplexConnection connection, PayloadDecoder payloadDecoder, Consumer errorConsumer, - StreamIdSupplier streamIdSupplier, - Duration tickPeriod, - Duration ackTimeout, - int missedAcks) { + StreamIdSupplier streamIdSupplier) { this.allocator = allocator; this.connection = connection; this.payloadDecoder = payloadDecoder; @@ -109,27 +85,6 @@ class RSocketClient implements RSocket { .subscribe(null, this::handleSendProcessorError); connection.receive().subscribe(this::handleIncomingFrames, errorConsumer); - - if (!Duration.ZERO.equals(tickPeriod)) { - this.keepAliveHandler = - KeepAliveHandler.ofClient( - new KeepAliveHandler.KeepAlive(tickPeriod, ackTimeout, missedAcks)); - - keepAliveHandler - .timeout() - .subscribe( - keepAlive -> { - String message = - String.format("No keep-alive acks for %d ms", keepAlive.getTimeoutMillis()); - ConnectionErrorException err = new ConnectionErrorException(message); - lifecycle.setTerminationError(err); - errorConsumer.accept(err); - connection.dispose(); - }); - keepAliveHandler.send().subscribe(sendProcessor::onNext); - } else { - keepAliveHandler = null; - } } private void handleSendProcessorError(Throwable t) { @@ -440,9 +395,6 @@ private boolean contains(int streamId) { protected void terminate() { lifecycle.setTerminationError(new ClosedChannelException()); - if (keepAliveHandler != null) { - keepAliveHandler.dispose(); - } try { receivers.values().forEach(this::cleanUpSubscriber); senders.values().forEach(this::cleanUpLimitableRequestPublisher); @@ -497,9 +449,8 @@ private void handleStreamZero(FrameType type, ByteBuf frame) { case LEASE: break; case KEEPALIVE: - if (keepAliveHandler != null) { - keepAliveHandler.receive(frame); - } + // KeepAlive is handled by corresponding connection interceptor, + // just release its frame here break; default: // Ignore unknown frames. Throwing an error will close the socket. diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 1e7b056ca..e3ea1a25e 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -20,17 +20,22 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.exceptions.InvalidSetupException; import io.rsocket.exceptions.RejectedSetupException; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.SetupFrameFlyweight; -import io.rsocket.frame.VersionFlyweight; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.ClientServerInputMultiplexer; +import io.rsocket.internal.ClientSetup; +import io.rsocket.internal.KeepAliveData; +import io.rsocket.internal.ServerSetup; +import io.rsocket.keepalive.KeepAliveConnection; import io.rsocket.plugins.DuplexConnectionInterceptor; import io.rsocket.plugins.PluginRegistry; import io.rsocket.plugins.Plugins; import io.rsocket.plugins.RSocketInterceptor; +import io.rsocket.resume.*; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; +import io.rsocket.util.ConnectionUtils; import io.rsocket.util.EmptyPayload; import java.time.Duration; import java.util.Objects; @@ -97,6 +102,16 @@ public static class ClientRSocketFactory implements ClientTransportAcceptor { private String metadataMimeType = "application/binary"; private String dataMimeType = "application/binary"; + private boolean resumeEnabled; + private Supplier resumeTokenSupplier = ResumeToken::generate; + private Function resumeStoreFactory = + token -> new InMemoryResumableFramesStore("client", 1024); + private Duration resumeSessionDuration = Duration.ofMinutes(2); + private Duration resumeStreamTimeout = Duration.ofSeconds(10); + private Supplier resumeStrategySupplier = + () -> + new ExponentialBackoffResumeStrategy(Duration.ofSeconds(1), Duration.ofSeconds(16), 2); + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; public ClientRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { @@ -169,6 +184,37 @@ public ClientRSocketFactory metadataMimeType(String metadataMimeType) { return this; } + public ClientRSocketFactory resume() { + this.resumeEnabled = true; + return this; + } + + public ClientRSocketFactory resumeToken(Supplier resumeTokenSupplier) { + this.resumeTokenSupplier = Objects.requireNonNull(resumeTokenSupplier); + return this; + } + + public ClientRSocketFactory resumeStore( + Function resumeStoreFactory) { + this.resumeStoreFactory = resumeStoreFactory; + return this; + } + + public ClientRSocketFactory resumeSessionDuration(Duration sessionDuration) { + this.resumeSessionDuration = Objects.requireNonNull(sessionDuration); + return this; + } + + public ClientRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout) { + this.resumeStreamTimeout = Objects.requireNonNull(resumeStreamTimeout); + return this; + } + + public ClientRSocketFactory resumeStrategy(Supplier resumeStrategy) { + this.resumeStrategySupplier = Objects.requireNonNull(resumeStrategy); + return this; + } + @Override public Start transport(Supplier transportClient) { return new StartClient(transportClient); @@ -213,25 +259,14 @@ private class StartClient implements Start { @Override public Mono start() { - return transportClient - .get() - .connect(mtu) + return newConnection() .flatMap( connection -> { - ByteBuf setupFrame = - SetupFrameFlyweight.encode( - allocator, - false, - false, - (int) tickPeriod.toMillis(), - (int) (ackTimeout.toMillis() + tickPeriod.toMillis() * missedAcks), - metadataMimeType, - dataMimeType, - setupPayload.sliceMetadata(), - setupPayload.sliceData()); + ClientSetup clientSetup = clientSetup(); + DuplexConnection wrappedConnection = clientSetup.wrappedConnection(connection); ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(connection, plugins); + new ClientServerInputMultiplexer(wrappedConnection, plugins); RSocketClient rSocketClient = new RSocketClient( @@ -239,10 +274,7 @@ public Mono start() { multiplexer.asClientConnection(), payloadDecoder, errorConsumer, - StreamIdSupplier.clientSupplier(), - tickPeriod, - ackTimeout, - missedAcks); + StreamIdSupplier.clientSupplier()); RSocket wrappedRSocketClient = plugins.applyClient(rSocketClient); @@ -258,9 +290,59 @@ public Mono start() { payloadDecoder, errorConsumer); - return connection.sendOne(setupFrame).thenReturn(wrappedRSocketClient); + ByteBuf setupFrame = + SetupFrameFlyweight.encode( + allocator, + false, + (int) keepAliveTickPeriod(), + (int) keepAliveTimeout(), + clientSetup.resumeToken().toByteBuf(), + metadataMimeType, + dataMimeType, + setupPayload.sliceMetadata(), + setupPayload.sliceData()); + + return wrappedConnection.sendOne(setupFrame).thenReturn(wrappedRSocketClient); }); } + + private long keepAliveTickPeriod() { + return tickPeriod.toMillis(); + } + + private long keepAliveTimeout() { + return ackTimeout.toMillis() + tickPeriod.toMillis() * missedAcks; + } + + private ClientSetup clientSetup() { + if (resumeEnabled) { + ResumeToken resumeToken = resumeTokenSupplier.get(); + return new ClientSetup.ResumableClientSetup( + allocator, + newConnection(), + resumeToken, + resumeStoreFactory.apply(resumeToken), + resumeSessionDuration, + resumeStreamTimeout, + resumeStrategySupplier); + } else { + return new ClientSetup.DefaultClientSetup(); + } + } + + private Mono newConnection() { + return transportClient + .get() + .connect(mtu) + .map( + connection -> + KeepAliveConnection.ofClient( + allocator, + connection, + notUsed -> + Mono.just(new KeepAliveData(keepAliveTickPeriod(), keepAliveTimeout())), + errorConsumer)); + } } } @@ -270,6 +352,12 @@ public static class ServerRSocketFactory { private Consumer errorConsumer = Throwable::printStackTrace; private int mtu = 0; private PluginRegistry plugins = new PluginRegistry(Plugins.defaultPlugins()); + private boolean resumeSupported; + private Duration resumeSessionDuration = Duration.ofSeconds(120); + private Duration resumeStreamTimeout = Duration.ofSeconds(10); + private Function resumeStoreFactory = + token -> new InMemoryResumableFramesStore("server", 1024); + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; private ServerRSocketFactory() {} @@ -315,6 +403,27 @@ public ServerRSocketFactory errorConsumer(Consumer errorConsumer) { return this; } + public ServerRSocketFactory resume() { + this.resumeSupported = true; + return this; + } + + public ServerRSocketFactory resumeStore( + Function resumeStoreFactory) { + this.resumeStoreFactory = resumeStoreFactory; + return this; + } + + public ServerRSocketFactory resumeSessionDuration(Duration sessionDuration) { + this.resumeSessionDuration = Objects.requireNonNull(sessionDuration); + return this; + } + + public ServerRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout) { + this.resumeStreamTimeout = Objects.requireNonNull(resumeStreamTimeout); + return this; + } + private class ServerStart implements Start { private final Supplier> transportServer; @@ -324,82 +433,141 @@ private class ServerStart implements Start { @Override public Mono start() { - return transportServer - .get() - .start( - connection -> { - ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(connection, plugins); - - return multiplexer - .asStreamZeroConnection() - .receive() - .next() - .flatMap(setupFrame -> processSetupFrame(multiplexer, setupFrame)); - }, - mtu); + return Mono.defer( + new Supplier>() { + + ServerSetup serverSetup = serverSetup(); + + @Override + public Mono get() { + return transportServer + .get() + .start( + connection -> { + connection = + KeepAliveConnection.ofServer( + allocator, + connection, + serverSetup.keepAliveData(), + errorConsumer); + ClientServerInputMultiplexer multiplexer = + new ClientServerInputMultiplexer(connection, plugins); + + return multiplexer + .asSetupConnection() + .receive() + .next() + .flatMap(startFrame -> accept(startFrame, multiplexer)); + }, + mtu) + .doOnNext(c -> c.onClose().doFinally(v -> serverSetup.dispose()).subscribe()); + } + + private Mono accept( + ByteBuf startFrame, ClientServerInputMultiplexer multiplexer) { + switch (FrameHeaderFlyweight.frameType(startFrame)) { + case SETUP: + return acceptSetup(startFrame, multiplexer); + case RESUME: + return acceptResume(startFrame, multiplexer); + default: + return acceptUnknown(startFrame, multiplexer); + } + } + + private Mono acceptSetup( + ByteBuf setupFrame, ClientServerInputMultiplexer multiplexer) { + + if (!SetupFrameFlyweight.isSupportedVersion(setupFrame)) { + return sendError( + multiplexer, + new InvalidSetupException( + "Unsupported version: " + + SetupFrameFlyweight.humanReadableVersion(setupFrame))) + .doFinally( + signalType -> { + setupFrame.release(); + multiplexer.dispose(); + }); + } + return serverSetup.acceptRSocketSetup( + setupFrame, + multiplexer, + wrappedMultiplexer -> { + ConnectionSetupPayload setupPayload = + ConnectionSetupPayload.create(setupFrame); + + RSocketClient rSocketClient = + new RSocketClient( + allocator, + wrappedMultiplexer.asServerConnection(), + payloadDecoder, + errorConsumer, + StreamIdSupplier.serverSupplier()); + + RSocket wrappedRSocketClient = plugins.applyClient(rSocketClient); + + return acceptor + .accept(setupPayload, wrappedRSocketClient) + .onErrorResume( + err -> + sendError(multiplexer, rejectedSetupError(err)) + .then(Mono.error(err))) + .doOnNext( + unwrappedServerSocket -> { + RSocket wrappedRSocketServer = + plugins.applyServer(unwrappedServerSocket); + + RSocketServer rSocketServer = + new RSocketServer( + allocator, + wrappedMultiplexer.asClientConnection(), + wrappedRSocketServer, + payloadDecoder, + errorConsumer); + }) + .doFinally(signalType -> setupPayload.release()) + .then(); + }); + } + + private Mono acceptResume( + ByteBuf resumeFrame, ClientServerInputMultiplexer multiplexer) { + return serverSetup.acceptRSocketResume(resumeFrame, multiplexer); + } + }); } - private Mono processSetupFrame( - ClientServerInputMultiplexer multiplexer, ByteBuf setupFrame) { - int version = SetupFrameFlyweight.version(setupFrame); - if (version != SetupFrameFlyweight.CURRENT_VERSION) { - setupFrame.release(); - InvalidSetupException error = - new InvalidSetupException( - "Unsupported version " + VersionFlyweight.toString(version)); - return multiplexer - .asStreamZeroConnection() - .sendOne(ErrorFrameFlyweight.encode(ByteBufAllocator.DEFAULT, 0, error)) - .doFinally(signalType -> multiplexer.dispose()); - } - - ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(setupFrame); - int keepAliveInterval = setupPayload.keepAliveInterval(); - int keepAliveMaxLifetime = setupPayload.keepAliveMaxLifetime(); - - RSocketClient rSocketClient = - new RSocketClient( + private ServerSetup serverSetup() { + return resumeSupported + ? new ServerSetup.ResumableServerSetup( allocator, - multiplexer.asServerConnection(), - payloadDecoder, - errorConsumer, - StreamIdSupplier.serverSupplier()); - - RSocket wrappedRSocketClient = plugins.applyClient(rSocketClient); - - return acceptor - .accept(setupPayload, wrappedRSocketClient) - .onErrorResume( - err -> - multiplexer - .asStreamZeroConnection() - .sendOne(rejectedSetupErrorFrame(err)) - .then(Mono.error(err))) - .doOnNext( - unwrappedServerSocket -> { - RSocket wrappedRSocketServer = plugins.applyServer(unwrappedServerSocket); + new SessionManager(), + resumeSessionDuration, + resumeStreamTimeout, + resumeStoreFactory) + : new ServerSetup.DefaultServerSetup(allocator); + } - RSocketServer rSocketServer = - new RSocketServer( - allocator, - multiplexer.asClientConnection(), - wrappedRSocketServer, - payloadDecoder, - errorConsumer, - keepAliveInterval, - keepAliveMaxLifetime); - }) - .doFinally(signalType -> setupPayload.release()) - .then(); + private Mono acceptUnknown(ByteBuf frame, ClientServerInputMultiplexer multiplexer) { + return sendError( + multiplexer, + new InvalidSetupException( + "invalid setup frame: " + FrameHeaderFlyweight.frameType(frame))) + .doFinally( + signalType -> { + frame.release(); + multiplexer.dispose(); + }); + } + + private Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { + return ConnectionUtils.sendError(allocator, multiplexer, exception); } - private ByteBuf rejectedSetupErrorFrame(Throwable err) { + private Exception rejectedSetupError(Throwable err) { String msg = err.getMessage(); - return ErrorFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, - 0, - new RejectedSetupException(msg == null ? "rejected by server acceptor" : msg)); + return new RejectedSetupException(msg == null ? "rejected by server acceptor" : msg); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java index 8a2fc1a60..a49feafa3 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java @@ -21,7 +21,6 @@ import io.netty.util.ReferenceCountUtil; import io.netty.util.collection.IntObjectHashMap; import io.rsocket.exceptions.ApplicationErrorException; -import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.frame.*; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.LimitableRequestPublisher; @@ -55,17 +54,6 @@ class RSocketServer implements ResponderRSocket { private final UnboundedProcessor sendProcessor; private final ByteBufAllocator allocator; - private KeepAliveHandler keepAliveHandler; - - /*client responder*/ - RSocketServer( - ByteBufAllocator allocator, - DuplexConnection connection, - RSocket requestHandler, - PayloadDecoder payloadDecoder, - Consumer errorConsumer) { - this(allocator, connection, requestHandler, payloadDecoder, errorConsumer, 0, 0); - } /*server responder*/ RSocketServer( @@ -73,9 +61,7 @@ class RSocketServer implements ResponderRSocket { DuplexConnection connection, RSocket requestHandler, PayloadDecoder payloadDecoder, - Consumer errorConsumer, - long tickPeriod, - long ackTimeout) { + Consumer errorConsumer) { this.allocator = allocator; this.connection = connection; @@ -114,24 +100,6 @@ class RSocketServer implements ResponderRSocket { receiveDisposable.dispose(); }) .subscribe(null, errorConsumer); - - if (tickPeriod != 0) { - keepAliveHandler = - KeepAliveHandler.ofServer(new KeepAliveHandler.KeepAlive(tickPeriod, ackTimeout)); - - keepAliveHandler - .timeout() - .subscribe( - keepAlive -> { - String message = - String.format("No keep-alive acks for %d ms", keepAlive.getTimeoutMillis()); - errorConsumer.accept(new ConnectionErrorException(message)); - connection.dispose(); - }); - keepAliveHandler.send().subscribe(sendProcessor::onNext); - } else { - keepAliveHandler = null; - } } private void handleSendProcessorError(Throwable t) { @@ -278,9 +246,6 @@ public Mono onClose() { } private void cleanup() { - if (keepAliveHandler != null) { - keepAliveHandler.dispose(); - } cleanUpSendingSubscriptions(); cleanUpChannelProcessors(); @@ -317,7 +282,8 @@ private void handleFrame(ByteBuf frame) { handleCancelFrame(streamId); break; case KEEPALIVE: - handleKeepAliveFrame(frame); + // KeepAlive is handled by corresponding connection interceptor, + // just release its frame here break; case REQUEST_N: handleRequestN(streamId, frame); @@ -474,12 +440,6 @@ private void handleChannel(int streamId, Payload payload, int initialRequestN) { } } - private void handleKeepAliveFrame(ByteBuf frame) { - if (keepAliveHandler != null) { - keepAliveHandler.receive(frame); - } - } - private void handleCancelFrame(int streamId) { Subscription subscription = sendingSubscriptions.remove(streamId); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java index f9ae72ed2..43cd821df 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java @@ -103,6 +103,6 @@ private static ByteBuf getData(ByteBuf frame, FrameType frameType) { default: return Unpooled.EMPTY_BUFFER; } - return data.retain(); + return data; } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ResumeFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ResumeFlyweight.java deleted file mode 100644 index 899957718..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/ResumeFlyweight.java +++ /dev/null @@ -1,3 +0,0 @@ -package io.rsocket.frame; - -public class ResumeFlyweight {} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameFlyweight.java new file mode 100644 index 000000000..91878b44b --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameFlyweight.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015-2019 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.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; + +public class ResumeFrameFlyweight { + static final int CURRENT_VERSION = SetupFrameFlyweight.CURRENT_VERSION; + + public static ByteBuf encode( + final ByteBufAllocator allocator, + byte[] token, + long lastReceivedServerPos, + long firstAvailableClientPos) { + + ByteBuf byteBuf = FrameHeaderFlyweight.encodeStreamZero(allocator, FrameType.RESUME, 0); + byteBuf.writeInt(CURRENT_VERSION); + byteBuf.writeShort(token.length); + byteBuf.writeBytes(token); + byteBuf.writeLong(lastReceivedServerPos); + byteBuf.writeLong(firstAvailableClientPos); + + return byteBuf; + } + + public static int version(ByteBuf byteBuf) { + FrameHeaderFlyweight.ensureFrameType(FrameType.RESUME, byteBuf); + + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderFlyweight.size()); + int version = byteBuf.readInt(); + byteBuf.resetReaderIndex(); + + return version; + } + + public static byte[] token(ByteBuf byteBuf) { + FrameHeaderFlyweight.ensureFrameType(FrameType.RESUME, byteBuf); + + byteBuf.markReaderIndex(); + // header + version + int tokenPos = FrameHeaderFlyweight.size() + Integer.BYTES; + byteBuf.skipBytes(tokenPos); + // token + int tokenLength = byteBuf.readShort() & 0xFFFF; + byte[] token = new byte[tokenLength]; + byteBuf.readBytes(token); + byteBuf.resetReaderIndex(); + + return token; + } + + public static long lastReceivedServerPos(ByteBuf byteBuf) { + FrameHeaderFlyweight.ensureFrameType(FrameType.RESUME, byteBuf); + + byteBuf.markReaderIndex(); + // header + version + int tokenPos = FrameHeaderFlyweight.size() + Integer.BYTES; + byteBuf.skipBytes(tokenPos); + // token + int tokenLength = byteBuf.readShort() & 0xFFFF; + byteBuf.skipBytes(tokenLength); + long lastReceivedServerPos = byteBuf.readLong(); + byteBuf.resetReaderIndex(); + + return lastReceivedServerPos; + } + + public static long firstAvailableClientPos(ByteBuf byteBuf) { + FrameHeaderFlyweight.ensureFrameType(FrameType.RESUME, byteBuf); + + byteBuf.markReaderIndex(); + // header + version + int tokenPos = FrameHeaderFlyweight.size() + Integer.BYTES; + byteBuf.skipBytes(tokenPos); + // token + int tokenLength = byteBuf.readShort() & 0xFFFF; + byteBuf.skipBytes(tokenLength); + // last received server position + byteBuf.skipBytes(Long.BYTES); + long firstAvailableClientPos = byteBuf.readLong(); + byteBuf.resetReaderIndex(); + + return firstAvailableClientPos; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFlyweight.java deleted file mode 100644 index d947a3f76..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFlyweight.java +++ /dev/null @@ -1,3 +0,0 @@ -package io.rsocket.frame; - -public class ResumeOkFlyweight {} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFrameFlyweight.java new file mode 100644 index 000000000..dd1971603 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFrameFlyweight.java @@ -0,0 +1,22 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; + +public class ResumeOkFrameFlyweight { + + public static ByteBuf encode(final ByteBufAllocator allocator, long lastReceivedClientPos) { + ByteBuf byteBuf = FrameHeaderFlyweight.encodeStreamZero(allocator, FrameType.RESUME_OK, 0); + byteBuf.writeLong(lastReceivedClientPos); + return byteBuf; + } + + public static long lastReceivedClientPos(ByteBuf byteBuf) { + FrameHeaderFlyweight.ensureFrameType(FrameType.RESUME_OK, byteBuf); + byteBuf.markReaderIndex(); + long lastReceivedClientPosition = byteBuf.skipBytes(FrameHeaderFlyweight.size()).readLong(); + byteBuf.resetReaderIndex(); + + return lastReceivedClientPosition; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java index 2a5992419..fc0142561 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java @@ -28,7 +28,6 @@ public class SetupFrameFlyweight { public static ByteBuf encode( final ByteBufAllocator allocator, boolean lease, - boolean resume, final int keepaliveInterval, final int maxLifetime, final String metadataMimeType, @@ -38,7 +37,6 @@ public static ByteBuf encode( return encode( allocator, lease, - resume, keepaliveInterval, maxLifetime, Unpooled.EMPTY_BUFFER, @@ -51,7 +49,6 @@ public static ByteBuf encode( public static ByteBuf encode( final ByteBufAllocator allocator, boolean lease, - boolean resume, final int keepaliveInterval, final int maxLifetime, final ByteBuf resumeToken, @@ -61,14 +58,10 @@ public static ByteBuf encode( final ByteBuf data) { int flags = 0; - if (resume) { - throw new IllegalArgumentException("RESUME_ENABLE not supported"); - } - /* - if (resume) { + if (resumeToken != null && resumeToken.readableBytes() > 0) { flags |= FLAGS_RESUME_ENABLE; - }*/ + } if (lease) { flags |= FLAGS_WILL_HONOR_LEASE; @@ -110,6 +103,15 @@ public static int version(ByteBuf byteBuf) { return version; } + public static String humanReadableVersion(ByteBuf byteBuf) { + int encodedVersion = version(byteBuf); + return VersionFlyweight.major(encodedVersion) + "." + VersionFlyweight.minor(encodedVersion); + } + + public static boolean isSupportedVersion(ByteBuf byteBuf) { + return CURRENT_VERSION == version(byteBuf); + } + public static int resumeTokenLength(ByteBuf byteBuf) { byteBuf.markReaderIndex(); int tokenLength = byteBuf.skipBytes(VARIABLE_DATA_OFFSET).readShort() & 0xFFFF; @@ -139,6 +141,32 @@ public static boolean resumeEnabled(ByteBuf byteBuf) { return (FLAGS_RESUME_ENABLE & FrameHeaderFlyweight.flags(byteBuf)) == FLAGS_RESUME_ENABLE; } + public static byte[] resumeToken(ByteBuf byteBuf) { + if (resumeEnabled(byteBuf)) { + byteBuf.markReaderIndex(); + // header + int resumePos = + FrameHeaderFlyweight.size() + + + // version + Integer.BYTES + + + // keep-alive interval + Integer.BYTES + + + // keep-alive maxLifeTime + Integer.BYTES; + + int tokenLength = byteBuf.skipBytes(resumePos).readShort() & 0xFFFF; + byte[] resumeToken = new byte[tokenLength]; + byteBuf.readBytes(resumeToken); + byteBuf.resetReaderIndex(); + return resumeToken; + } else { + return null; + } + } + public static String metadataMimeType(ByteBuf byteBuf) { int skip = bytesToSkipToMimeType(byteBuf); byteBuf.markReaderIndex(); @@ -179,7 +207,7 @@ public static ByteBuf data(ByteBuf byteBuf) { private static int bytesToSkipToMimeType(ByteBuf byteBuf) { int bytesToSkip = VARIABLE_DATA_OFFSET; if ((FLAGS_RESUME_ENABLE & FrameHeaderFlyweight.flags(byteBuf)) == FLAGS_RESUME_ENABLE) { - bytesToSkip = resumeTokenLength(byteBuf) + Short.BYTES; + bytesToSkip += resumeTokenLength(byteBuf) + Short.BYTES; } return bytesToSkip; } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerConnection.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerConnection.java new file mode 100644 index 000000000..3043e8082 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerConnection.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015-2019 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.internal; + +import io.rsocket.DuplexConnection; +import io.rsocket.resume.ResumeAwareConnection; +import io.rsocket.resume.ResumeStateHolder; +import io.rsocket.util.DuplexConnectionProxy; +import reactor.core.publisher.Flux; + +class ClientServerConnection extends DuplexConnectionProxy implements ResumeAwareConnection { + + private final DuplexConnection resumeAware; + + public ClientServerConnection(DuplexConnection delegate, DuplexConnection resumeAware) { + super(delegate); + this.resumeAware = resumeAware; + } + + @Override + public Flux receiveResumePositions(ResumeStateHolder resumeStateHolder) { + return resumeAware instanceof ResumeAwareConnection + ? ((ResumeAwareConnection) resumeAware).receiveResumePositions(resumeStateHolder) + : Flux.never(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index 5c0c1d74f..e132ade40 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -20,10 +20,10 @@ import io.rsocket.Closeable; import io.rsocket.DuplexConnection; import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.FrameType; import io.rsocket.frame.FrameUtil; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; import io.rsocket.plugins.PluginRegistry; +import io.rsocket.resume.ResumeAwareConnection; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -46,25 +46,33 @@ */ public class ClientServerInputMultiplexer implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger("io.rsocket.FrameLogger"); + private static final PluginRegistry emptyPluginRegistry = new PluginRegistry(); - private final DuplexConnection streamZeroConnection; + private final DuplexConnection setupConnection; private final DuplexConnection serverConnection; private final DuplexConnection clientConnection; private final DuplexConnection source; + private final ResumeAwareConnection clientServerConnection; + + public ClientServerInputMultiplexer(DuplexConnection source) { + this(source, emptyPluginRegistry); + } public ClientServerInputMultiplexer(DuplexConnection source, PluginRegistry plugins) { this.source = source; - final MonoProcessor> streamZero = MonoProcessor.create(); + final MonoProcessor> setup = MonoProcessor.create(); final MonoProcessor> server = MonoProcessor.create(); final MonoProcessor> client = MonoProcessor.create(); source = plugins.applyConnection(Type.SOURCE, source); - streamZeroConnection = - plugins.applyConnection(Type.STREAM_ZERO, new InternalDuplexConnection(source, streamZero)); + setupConnection = + plugins.applyConnection(Type.SETUP, new InternalDuplexConnection(source, setup)); serverConnection = plugins.applyConnection(Type.SERVER, new InternalDuplexConnection(source, server)); clientConnection = plugins.applyConnection(Type.CLIENT, new InternalDuplexConnection(source, client)); + clientServerConnection = + new ClientServerConnection(new InternalDuplexConnection(source, client, server), source); source .receive() @@ -73,8 +81,8 @@ public ClientServerInputMultiplexer(DuplexConnection source, PluginRegistry plug int streamId = FrameHeaderFlyweight.streamId(frame); final Type type; if (streamId == 0) { - if (FrameHeaderFlyweight.frameType(frame) == FrameType.SETUP) { - type = Type.STREAM_ZERO; + if (isSetup(frame)) { + type = Type.SETUP; } else { type = Type.CLIENT; } @@ -88,8 +96,8 @@ public ClientServerInputMultiplexer(DuplexConnection source, PluginRegistry plug .subscribe( group -> { switch (group.key()) { - case STREAM_ZERO: - streamZero.onNext(group); + case SETUP: + setup.onNext(group); break; case SERVER: @@ -107,6 +115,10 @@ public ClientServerInputMultiplexer(DuplexConnection source, PluginRegistry plug }); } + public ResumeAwareConnection asClientServerConnection() { + return clientServerConnection; + } + public DuplexConnection asServerConnection() { return serverConnection; } @@ -115,8 +127,8 @@ public DuplexConnection asClientConnection() { return clientConnection; } - public DuplexConnection asStreamZeroConnection() { - return streamZeroConnection; + public DuplexConnection asSetupConnection() { + return setupConnection; } @Override @@ -134,15 +146,26 @@ public Mono onClose() { return source.onClose(); } + private static boolean isSetup(ByteBuf frame) { + switch (FrameHeaderFlyweight.frameType(frame)) { + case SETUP: + case RESUME: + case RESUME_OK: + return true; + default: + return false; + } + } + private static class InternalDuplexConnection implements DuplexConnection { private final DuplexConnection source; - private final MonoProcessor> processor; + private final MonoProcessor>[] processors; private final boolean debugEnabled; public InternalDuplexConnection( - DuplexConnection source, MonoProcessor> processor) { + DuplexConnection source, MonoProcessor>... processors) { this.source = source; - this.processor = processor; + this.processors = processors; this.debugEnabled = LOGGER.isDebugEnabled(); } @@ -166,14 +189,18 @@ public Mono sendOne(ByteBuf frame) { @Override public Flux receive() { - return processor.flatMapMany( - f -> { - if (debugEnabled) { - return f.doOnNext(frame -> LOGGER.debug("receiving -> " + FrameUtil.toString(frame))); - } else { - return f; - } - }); + return Flux.fromArray(processors) + .flatMap( + p -> + p.flatMapMany( + f -> { + if (debugEnabled) { + return f.doOnNext( + frame -> LOGGER.debug("receiving -> " + FrameUtil.toString(frame))); + } else { + return f; + } + })); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java new file mode 100644 index 000000000..3309fc83c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java @@ -0,0 +1,87 @@ +/* + * Copyright 2015-2019 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.internal; + +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.keepalive.KeepAliveConnection; +import io.rsocket.resume.*; +import java.time.Duration; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +public interface ClientSetup { + /*Provide different connections for SETUP / RESUME cases*/ + DuplexConnection wrappedConnection(KeepAliveConnection duplexConnection); + + /*Provide different resume tokens for SETUP / RESUME cases*/ + ResumeToken resumeToken(); + + class DefaultClientSetup implements ClientSetup { + + @Override + public DuplexConnection wrappedConnection(KeepAliveConnection connection) { + return connection; + } + + @Override + public ResumeToken resumeToken() { + return ResumeToken.empty(); + } + } + + class ResumableClientSetup implements ClientSetup { + private final ResumeToken resumeToken; + private final ClientResumeConfiguration config; + private final ByteBufAllocator allocator; + private final Mono newConnection; + + public ResumableClientSetup( + ByteBufAllocator allocator, + Mono newConnection, + ResumeToken resumeToken, + ResumableFramesStore resumableFramesStore, + Duration resumeSessionDuration, + Duration resumeStreamTimeout, + Supplier resumeStrategySupplier) { + this.allocator = allocator; + this.newConnection = newConnection; + this.resumeToken = resumeToken; + this.config = + new ClientResumeConfiguration( + resumeSessionDuration, + resumeStrategySupplier, + resumableFramesStore, + resumeStreamTimeout); + } + + @Override + public DuplexConnection wrappedConnection(KeepAliveConnection connection) { + ClientRSocketSession rSocketSession = + new ClientRSocketSession(allocator, connection, config) + .continueWith(newConnection) + .resumeWith(resumeToken); + + return rSocketSession.resumableConnection(); + } + + @Override + public ResumeToken resumeToken() { + return resumeToken; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/KeepAliveData.java b/rsocket-core/src/main/java/io/rsocket/internal/KeepAliveData.java new file mode 100644 index 000000000..25338df31 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/KeepAliveData.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015-2019 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.internal; + +import java.time.Duration; + +public class KeepAliveData { + private final Duration tickPeriod; + private final Duration timeout; + + public KeepAliveData(long tickPeriodMillis, long timeoutMillis) { + this(Duration.ofMillis(tickPeriodMillis), Duration.ofMillis(timeoutMillis)); + } + + public KeepAliveData(Duration tickPeriod, Duration timeout) { + this.tickPeriod = tickPeriod; + this.timeout = timeout; + } + + public Duration getTickPeriod() { + return tickPeriod; + } + + public Duration getTimeout() { + return timeout; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java b/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java new file mode 100644 index 000000000..11cc5d1d3 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java @@ -0,0 +1,200 @@ +/* + * Copyright 2015-2019 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.internal; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.exceptions.RejectedResumeException; +import io.rsocket.exceptions.UnsupportedSetupException; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.ResumeFrameFlyweight; +import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.resume.*; +import io.rsocket.util.ConnectionUtils; +import java.time.Duration; +import java.util.function.Function; +import reactor.core.publisher.Mono; + +public interface ServerSetup { + /*accept connection as SETUP*/ + Mono acceptRSocketSetup( + ByteBuf frame, + ClientServerInputMultiplexer multiplexer, + Function> then); + + /*accept connection as RESUME*/ + Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer); + + /*get KEEP-ALIVE timings based on start frame: SETUP (directly) /RESUME (lookup by resume token)*/ + Function> keepAliveData(); + + default void dispose() {} + + class DefaultServerSetup implements ServerSetup { + private final ByteBufAllocator allocator; + + public DefaultServerSetup(ByteBufAllocator allocator) { + this.allocator = allocator; + } + + @Override + public Mono acceptRSocketSetup( + ByteBuf frame, + ClientServerInputMultiplexer multiplexer, + Function> then) { + + if (!ResumeToken.fromBytes(SetupFrameFlyweight.resumeToken(frame)).isEmpty()) { + return sendError(multiplexer, new UnsupportedSetupException("resume not supported")) + .doFinally( + signalType -> { + frame.release(); + multiplexer.dispose(); + }); + } else { + return then.apply(multiplexer); + } + } + + @Override + public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer) { + + return sendError(multiplexer, new RejectedResumeException("resume not supported")) + .doFinally( + signalType -> { + frame.release(); + multiplexer.dispose(); + }); + } + + @Override + public Function> keepAliveData() { + return frame -> { + if (FrameHeaderFlyweight.frameType(frame) == FrameType.SETUP) { + return Mono.just( + new KeepAliveData( + SetupFrameFlyweight.keepAliveInterval(frame), + SetupFrameFlyweight.keepAliveMaxLifetime(frame))); + } else { + return Mono.never(); + } + }; + } + + private Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { + return ConnectionUtils.sendError(allocator, multiplexer, exception); + } + } + + class ResumableServerSetup implements ServerSetup { + private final ByteBufAllocator allocator; + private final SessionManager sessionManager; + private final ServerResumeConfiguration resumeConfig; + + public ResumableServerSetup( + ByteBufAllocator allocator, + SessionManager sessionManager, + Duration resumeSessionDuration, + Duration resumeStreamTimeout, + Function resumeStoreFactory) { + this.allocator = allocator; + this.sessionManager = sessionManager; + this.resumeConfig = + new ServerResumeConfiguration( + resumeSessionDuration, resumeStreamTimeout, resumeStoreFactory); + } + + @Override + public Mono acceptRSocketSetup( + ByteBuf frame, + ClientServerInputMultiplexer multiplexer, + Function> then) { + + ResumeToken token = ResumeToken.fromBytes(SetupFrameFlyweight.resumeToken(frame)); + if (!token.isEmpty()) { + + KeepAliveData keepAliveData = + new KeepAliveData( + SetupFrameFlyweight.keepAliveInterval(frame), + SetupFrameFlyweight.keepAliveMaxLifetime(frame)); + + DuplexConnection resumableConnection = + sessionManager + .save( + new ServerRSocketSession( + allocator, + multiplexer.asClientServerConnection(), + resumeConfig, + keepAliveData, + token)) + .resumableConnection(); + return then.apply(new ClientServerInputMultiplexer(resumableConnection)); + } else { + return then.apply(multiplexer); + } + } + + @Override + public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer) { + return sessionManager + .get(ResumeToken.fromBytes(ResumeFrameFlyweight.token(frame))) + .map( + session -> + session + .continueWith(multiplexer.asClientServerConnection()) + .resumeWith(frame) + .onClose() + .then()) + .orElseGet( + () -> + sendError(multiplexer, new RejectedResumeException("unknown resume token")) + .doFinally( + s -> { + frame.release(); + multiplexer.dispose(); + })); + } + + @Override + public Function> keepAliveData() { + return frame -> { + if (FrameHeaderFlyweight.frameType(frame) == FrameType.SETUP) { + return Mono.just( + new KeepAliveData( + SetupFrameFlyweight.keepAliveInterval(frame), + SetupFrameFlyweight.keepAliveMaxLifetime(frame))); + } else { + return sessionManager + .get(ResumeToken.fromBytes(ResumeFrameFlyweight.token(frame))) + .map(ServerRSocketSession::keepAliveData) + .map(Mono::just) + .orElseGet(Mono::never); + } + }; + } + + private Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { + return ConnectionUtils.sendError(allocator, multiplexer, exception); + } + + @Override + public void dispose() { + sessionManager.dispose(); + } + } +} 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 88d486174..1affeb8fd 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -16,7 +16,7 @@ package io.rsocket.internal; -import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; import java.util.Objects; import java.util.Queue; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; @@ -192,7 +192,7 @@ boolean checkTerminated(boolean d, boolean empty, Subscriber a, Queue while (!q.isEmpty()) { T t = q.poll(); if (t != null) { - ReferenceCountUtil.safeRelease(t); + release(t); } } actual = null; @@ -240,7 +240,7 @@ public Context currentContext() { public void onNext(T t) { if (done || cancelled) { Operators.onNextDropped(t, currentContext()); - ReferenceCountUtil.safeRelease(t); + release(t); return; } @@ -248,7 +248,7 @@ public void onNext(T t) { Throwable ex = Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); onError(Operators.onOperatorError(null, ex, t, currentContext())); - ReferenceCountUtil.safeRelease(t); + release(t); return; } drain(); @@ -344,7 +344,7 @@ public void clear() { while (!queue.isEmpty()) { T t = queue.poll(); if (t != null) { - ReferenceCountUtil.safeRelease(t); + release(t); } } } @@ -388,4 +388,13 @@ public long downstreamCount() { public boolean hasDownstreams() { return actual != null; } + + void release(T t) { + if (t instanceof ReferenceCounted) { + ReferenceCounted refCounted = (ReferenceCounted) t; + if (refCounted.refCnt() > 0) { + refCounted.release(); + } + } + } } diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveConnection.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveConnection.java new file mode 100644 index 000000000..76ad66a60 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveConnection.java @@ -0,0 +1,164 @@ +/* + * Copyright 2015-2019 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.keepalive; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.exceptions.ConnectionErrorException; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameType; +import io.rsocket.internal.KeepAliveData; +import io.rsocket.resume.ResumeAwareConnection; +import io.rsocket.resume.ResumeStateHolder; +import io.rsocket.util.DuplexConnectionProxy; +import io.rsocket.util.Function3; +import java.time.Duration; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import org.reactivestreams.Publisher; +import reactor.core.publisher.*; + +public class KeepAliveConnection extends DuplexConnectionProxy implements ResumeAwareConnection { + + private final MonoProcessor keepAliveHandlerReady = MonoProcessor.create(); + private final ByteBufAllocator allocator; + private final Function> keepAliveData; + private final Function3 + keepAliveHandlerFactory; + private final Consumer errorConsumer; + private final UnicastProcessor keepAliveFrames = UnicastProcessor.create(); + private final EmitterProcessor lastReceivedPositions = EmitterProcessor.create(); + private volatile KeepAliveHandler keepAliveHandler; + + public static KeepAliveConnection ofClient( + ByteBufAllocator allocator, + DuplexConnection duplexConnection, + Function> keepAliveData, + Consumer errorConsumer) { + + return new KeepAliveConnection( + allocator, duplexConnection, keepAliveData, KeepAliveHandler::ofClient, errorConsumer); + } + + public static KeepAliveConnection ofServer( + ByteBufAllocator allocator, + DuplexConnection duplexConnection, + Function> keepAliveData, + Consumer errorConsumer) { + + return new KeepAliveConnection( + allocator, duplexConnection, keepAliveData, KeepAliveHandler::ofServer, errorConsumer); + } + + private KeepAliveConnection( + ByteBufAllocator allocator, + DuplexConnection duplexConnection, + Function> keepAliveData, + Function3 keepAliveHandlerFactory, + Consumer errorConsumer) { + super(duplexConnection); + this.allocator = allocator; + this.keepAliveData = keepAliveData; + this.keepAliveHandlerFactory = keepAliveHandlerFactory; + this.errorConsumer = errorConsumer; + keepAliveHandlerReady.subscribe(this::startKeepAlives); + } + + private void startKeepAlives(KeepAliveHandler keepAliveHandler) { + this.keepAliveHandler = keepAliveHandler; + send(keepAliveFrames).subscribe(null, err -> keepAliveHandler.dispose()); + + keepAliveHandler + .timeout() + .subscribe( + keepAlive -> { + String message = + String.format("No keep-alive acks for %d ms", keepAlive.getTimeoutMillis()); + ConnectionErrorException err = new ConnectionErrorException(message); + errorConsumer.accept(err); + dispose(); + }); + keepAliveHandler.send().subscribe(keepAliveFrames::onNext); + keepAliveHandler.start(); + } + + @Override + public Mono send(Publisher frames) { + return super.send( + Flux.from(frames) + .doOnNext( + f -> { + if (isStartFrame(f)) { + keepAliveHandler(keepAliveData.apply(f)).subscribe(keepAliveHandlerReady); + } + })); + } + + @Override + public Flux receive() { + return super.receive() + .doOnNext( + f -> { + if (isKeepAliveFrame(f)) { + long receivedPos = keepAliveHandler.receive(f); + if (isResumeRequested() && receivedPos > 0) { + lastReceivedPositions.onNext(receivedPos); + } + } else if (isStartFrame(f)) { + keepAliveHandler(keepAliveData.apply(f)).subscribe(keepAliveHandlerReady); + } + }); + } + + @Override + public Mono onClose() { + return super.onClose() + .then(Mono.fromRunnable(lastReceivedPositions::onComplete)) + .then( + Mono.fromRunnable( + () -> + Optional.ofNullable(keepAliveHandlerReady.peek()) + .ifPresent(KeepAliveHandler::dispose))); + } + + @Override + public Flux receiveResumePositions(ResumeStateHolder resumeStateHolder) { + return keepAliveHandlerReady + .doOnNext(h -> h.resumeState(resumeStateHolder)) + .thenMany(lastReceivedPositions); + } + + private boolean isResumeRequested() { + return keepAliveHandler.hasResumeState(); + } + + private static boolean isStartFrame(ByteBuf frame) { + return FrameHeaderFlyweight.frameType(frame) == FrameType.SETUP + || FrameHeaderFlyweight.frameType(frame) == FrameType.RESUME; + } + + private static boolean isKeepAliveFrame(ByteBuf frame) { + return FrameHeaderFlyweight.frameType(frame) == FrameType.KEEPALIVE; + } + + private Mono keepAliveHandler(Mono keepAliveData) { + return keepAliveData.map( + kad -> keepAliveHandlerFactory.apply(allocator, kad.getTickPeriod(), kad.getTimeout())); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java new file mode 100644 index 000000000..9385ee1c0 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java @@ -0,0 +1,169 @@ +/* + * Copyright 2015-2019 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.keepalive; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.frame.KeepAliveFrameFlyweight; +import io.rsocket.resume.ResumeStateHolder; +import java.time.Duration; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicReference; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.UnicastProcessor; + +abstract class KeepAliveHandler implements Disposable { + protected final ByteBufAllocator allocator; + private final Duration keepAlivePeriod; + private final Duration keepAliveTimeout; + private volatile Optional resumeStateHolder = Optional.empty(); + private final UnicastProcessor sent = UnicastProcessor.create(); + private final MonoProcessor timeout = MonoProcessor.create(); + private final AtomicReference intervalDisposable = new AtomicReference<>(); + private volatile long lastReceivedMillis; + + static KeepAliveHandler ofServer( + ByteBufAllocator allocator, Duration keepAlivePeriod, Duration keepAliveTimeout) { + return new KeepAliveHandler.Server(allocator, keepAlivePeriod, keepAliveTimeout); + } + + static KeepAliveHandler ofClient( + ByteBufAllocator allocator, Duration keepAlivePeriod, Duration keepAliveTimeout) { + return new KeepAliveHandler.Client(allocator, keepAlivePeriod, keepAliveTimeout); + } + + private KeepAliveHandler( + ByteBufAllocator allocator, Duration keepAlivePeriod, Duration keepAliveTimeout) { + this.allocator = allocator; + this.keepAlivePeriod = keepAlivePeriod; + this.keepAliveTimeout = keepAliveTimeout; + } + + public void start() { + this.lastReceivedMillis = System.currentTimeMillis(); + intervalDisposable.compareAndSet( + null, Flux.interval(keepAlivePeriod).subscribe(v -> onIntervalTick())); + } + + @Override + public void dispose() { + Disposable d = intervalDisposable.getAndSet(Disposables.disposed()); + if (d != null) { + d.dispose(); + } + sent.onComplete(); + timeout.onComplete(); + } + + public long receive(ByteBuf keepAliveFrame) { + this.lastReceivedMillis = System.currentTimeMillis(); + long remoteLastReceivedPos = KeepAliveFrameFlyweight.lastPosition(keepAliveFrame); + if (KeepAliveFrameFlyweight.respondFlag(keepAliveFrame)) { + long localLastReceivedPos = obtainLastReceivedPos(); + doSend( + KeepAliveFrameFlyweight.encode( + allocator, + false, + localLastReceivedPos, + KeepAliveFrameFlyweight.data(keepAliveFrame).retain())); + } + return remoteLastReceivedPos; + } + + public void resumeState(ResumeStateHolder resumeStateHolder) { + this.resumeStateHolder = Optional.of(resumeStateHolder); + } + + public boolean hasResumeState() { + return resumeStateHolder.isPresent(); + } + + public Flux send() { + return sent; + } + + public Mono timeout() { + return timeout; + } + + abstract void onIntervalTick(); + + void doSend(ByteBuf frame) { + sent.onNext(frame); + } + + void doCheckTimeout() { + long now = System.currentTimeMillis(); + if (now - lastReceivedMillis >= keepAliveTimeout.toMillis()) { + timeout.onNext(new KeepAlive(keepAlivePeriod.toMillis(), keepAliveTimeout.toMillis())); + } + } + + Long obtainLastReceivedPos() { + return resumeStateHolder.map(ResumeStateHolder::impliedPosition).orElse(0L); + } + + private static class Server extends KeepAliveHandler { + + Server(ByteBufAllocator allocator, Duration keepAlivePeriod, Duration keepAliveTimeout) { + super(allocator, keepAlivePeriod, keepAliveTimeout); + } + + @Override + void onIntervalTick() { + doCheckTimeout(); + } + } + + private static final class Client extends KeepAliveHandler { + + Client(ByteBufAllocator allocator, Duration keepAlivePeriod, Duration keepAliveTimeout) { + super(allocator, keepAlivePeriod, keepAliveTimeout); + } + + @Override + void onIntervalTick() { + doCheckTimeout(); + doSend( + KeepAliveFrameFlyweight.encode( + allocator, true, obtainLastReceivedPos(), Unpooled.EMPTY_BUFFER)); + } + } + + public static final class KeepAlive { + private final long tickPeriod; + private final long timeoutMillis; + + public KeepAlive(long tickPeriod, long timeoutMillis) { + this.tickPeriod = tickPeriod; + this.timeoutMillis = timeoutMillis; + } + + public long getTickPeriod() { + return tickPeriod; + } + + public long getTimeoutMillis() { + return timeoutMillis; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java index 2b10f8746..056ded0cd 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java @@ -23,7 +23,7 @@ public @FunctionalInterface interface DuplexConnectionInterceptor extends BiFunction { enum Type { - STREAM_ZERO, + SETUP, CLIENT, SERVER, SOURCE diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java new file mode 100644 index 000000000..c16674744 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -0,0 +1,166 @@ +/* + * Copyright 2015-2019 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.resume; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.exceptions.ConnectionErrorException; +import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ResumeFrameFlyweight; +import io.rsocket.frame.ResumeOkFrameFlyweight; +import io.rsocket.internal.ClientServerInputMultiplexer; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Mono; + +public class ClientRSocketSession implements RSocketSession> { + private static final Logger logger = LoggerFactory.getLogger(ClientRSocketSession.class); + + private final ResumableDuplexConnection resumableConnection; + private volatile Mono newConnection; + private volatile ResumeToken resumeToken; + private final ByteBufAllocator allocator; + + public ClientRSocketSession( + ByteBufAllocator allocator, + ResumeAwareConnection duplexConnection, + ClientResumeConfiguration config) { + this.allocator = Objects.requireNonNull(allocator); + this.resumableConnection = + new ResumableDuplexConnection( + "client", + duplexConnection, + ResumedFramesCalculator.ofClient, + config.resumeStore(), + config.resumeStreamTimeout()); + + resumableConnection + .connectionErrors() + .flatMap( + err -> { + logger.debug("Client session connection error. Starting new connection"); + ResumeStrategy reconnectOnError = config.resumptionStrategy().get(); + ClientResume clientResume = new ClientResume(config.sessionDuration(), resumeToken); + AtomicBoolean once = new AtomicBoolean(); + return newConnection + .delaySubscription( + once.compareAndSet(false, true) + ? reconnectOnError.apply(clientResume, err) + : Mono.empty()) + .retryWhen( + errors -> + errors + .doOnNext( + retryErr -> + logger.debug("Resumption reconnection error: {}", retryErr)) + .flatMap( + retryErr -> + Mono.from(reconnectOnError.apply(clientResume, retryErr)) + .doOnNext(v -> logger.debug("Retrying with: {}", v)))) + .timeout(config.sessionDuration()); + }) + .map(ClientServerInputMultiplexer::new) + .subscribe( + multiplexer -> { + /*reconnect resumable connection*/ + reconnect(multiplexer.asClientServerConnection()); + + ResumptionState state = resumableConnection.state(); + logger.debug( + "Client ResumableConnection reconnected. Sending RESUME frame with state: {}", + state); + /*Connection is established again: send RESUME frame to server, listen for RESUME_OK*/ + sendFrame( + ResumeFrameFlyweight.encode( + allocator, + resumeToken.toByteArray(), + state.impliedPosition(), + state.position())) + .then(multiplexer.asSetupConnection().receive().next()) + .subscribe(this::resumeWith); + }, + err -> { + logger.debug("Client ResumableConnection reconnect timeout"); + resumableConnection.dispose(); + }); + } + + @Override + public ClientRSocketSession continueWith(Mono newConnection) { + this.newConnection = newConnection; + return this; + } + + @Override + public ClientRSocketSession resumeWith(ByteBuf resumeOkFrame) { + logger.debug("ResumeOK FRAME received"); + ResumptionState resumptionState = stateFromFrame(resumeOkFrame); + resumableConnection.resume( + resumptionState, + pos -> + pos.then() + /*Resumption is impossible: send CONNECTION_ERROR*/ + .onErrorResume( + err -> + sendFrame( + ErrorFrameFlyweight.encode( + allocator, + 0, + errorFrameThrowable(resumptionState.impliedPosition()))) + .then(Mono.fromRunnable(resumableConnection::dispose)) + /*Resumption is impossible: no need to return control to ResumableConnection*/ + .then(Mono.never()))); + return this; + } + + public ClientRSocketSession resumeWith(ResumeToken resumeToken) { + this.resumeToken = resumeToken; + return this; + } + + @Override + public void reconnect(ResumeAwareConnection connection) { + resumableConnection.reconnect(connection); + } + + @Override + public DuplexConnection resumableConnection() { + return resumableConnection; + } + + @Override + public ResumeToken token() { + return resumeToken; + } + + private Mono sendFrame(ByteBuf frame) { + return resumableConnection.sendOne(frame).onErrorResume(err -> Mono.empty()); + } + + private static ResumptionState stateFromFrame(ByteBuf resumeOkFrame) { + long impliedPos = ResumeOkFrameFlyweight.lastReceivedClientPos(resumeOkFrame); + resumeOkFrame.release(); + return ResumptionState.fromServer(impliedPos); + } + + private static ConnectionErrorException errorFrameThrowable(long impliedPos) { + return new ConnectionErrorException("resumption_server_pos=[" + impliedPos + "]"); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ResumeTokenTest.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java similarity index 51% rename from rsocket-core/src/test/java/io/rsocket/resume/ResumeTokenTest.java rename to rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java index 7a2fafc8a..f757447d2 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ResumeTokenTest.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2019 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. @@ -16,22 +16,22 @@ package io.rsocket.resume; -import static org.junit.Assert.assertEquals; +import java.time.Duration; -import java.util.UUID; -import org.junit.Test; +class ClientResume { + private final Duration sessionDuration; + private final ResumeToken resumeToken; -public class ResumeTokenTest { - @Test - public void testFromUuid() { - UUID x = UUID.fromString("3bac9870-3873-403a-99f4-9728aa8c7860"); - - ResumeToken t = ResumeToken.bytes(ResumeToken.getBytesFromUUID(x)); - ResumeToken t2 = ResumeToken.bytes(ResumeToken.getBytesFromUUID(x)); + public ClientResume(Duration sessionDuration, ResumeToken resumeToken) { + this.sessionDuration = sessionDuration; + this.resumeToken = resumeToken; + } - assertEquals("3bac98703873403a99f49728aa8c7860", t.toString()); + public Duration sessionDuration() { + return sessionDuration; + } - assertEquals(t.hashCode(), t2.hashCode()); - assertEquals(t, t2); + public ResumeToken resumeToken() { + return resumeToken; } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientResumeConfiguration.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientResumeConfiguration.java new file mode 100644 index 000000000..0eb8b20d3 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientResumeConfiguration.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015-2019 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.resume; + +import java.time.Duration; +import java.util.function.Supplier; + +public class ClientResumeConfiguration { + private final Duration sessionDuration; + private final Supplier resumeStrategy; + private final ResumableFramesStore resumableFramesStore; + private final Duration resumeStreamTimeout; + + public ClientResumeConfiguration( + Duration sessionDuration, + Supplier resumeStrategy, + ResumableFramesStore resumableFramesStore, + Duration resumeStreamTimeout) { + this.sessionDuration = sessionDuration; + this.resumeStrategy = resumeStrategy; + this.resumableFramesStore = resumableFramesStore; + this.resumeStreamTimeout = resumeStreamTimeout; + } + + public Duration sessionDuration() { + return sessionDuration; + } + + public Supplier resumptionStrategy() { + return resumeStrategy; + } + + public ResumableFramesStore resumeStore() { + return resumableFramesStore; + } + + public Duration resumeStreamTimeout() { + return resumeStreamTimeout; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ExponentialBackoffResumeStrategy.java b/rsocket-core/src/main/java/io/rsocket/resume/ExponentialBackoffResumeStrategy.java new file mode 100644 index 000000000..b46ac864b --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ExponentialBackoffResumeStrategy.java @@ -0,0 +1,71 @@ +/* + * Copyright 2015-2019 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.resume; + +import java.time.Duration; +import java.util.Objects; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class ExponentialBackoffResumeStrategy implements ResumeStrategy { + private volatile Duration next; + private final Duration firstBackoff; + private final Duration maxBackoff; + private final int factor; + + public ExponentialBackoffResumeStrategy(Duration firstBackoff, Duration maxBackoff, int factor) { + this.firstBackoff = Objects.requireNonNull(firstBackoff, "firstBackoff"); + this.maxBackoff = Objects.requireNonNull(maxBackoff, "maxBackoff"); + this.factor = requirePositive(factor); + } + + @Override + public Publisher apply(ClientResume clientResume, Throwable throwable) { + return Flux.defer(() -> Mono.delay(next()).thenReturn(toString())); + } + + Duration next() { + next = + next == null + ? firstBackoff + : Duration.ofMillis(Math.min(maxBackoff.toMillis(), next.toMillis() * factor)); + return next; + } + + private static int requirePositive(int value) { + if (value <= 0) { + throw new IllegalArgumentException("Value must be positive: " + value); + } else { + return value; + } + } + + @Override + public String toString() { + return "ExponentialBackoffResumeStrategy{" + + "next=" + + next + + ", firstBackoff=" + + firstBackoff + + ", maxBackoff=" + + maxBackoff + + ", factor=" + + factor + + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java new file mode 100644 index 000000000..b241701cd --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -0,0 +1,145 @@ +/* + * Copyright 2015-2019 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.resume; + +import io.netty.buffer.ByteBuf; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.util.concurrent.Queues; + +public class InMemoryResumableFramesStore implements ResumableFramesStore { + private static final Logger logger = LoggerFactory.getLogger(InMemoryResumableFramesStore.class); + + private final MonoProcessor disposed = MonoProcessor.create(); + private final AtomicLong position = new AtomicLong(); + private final AtomicLong impliedPosition = new AtomicLong(); + private final AtomicInteger cachedFramesSize = new AtomicInteger(); + private final Queue cachedFrames; + private final String tag; + private final int cacheLimit; + private final AtomicInteger upstreamFrameRefCnt = new AtomicInteger(); + + public InMemoryResumableFramesStore(String tag, int cacheLimit) { + this.tag = tag; + this.cacheLimit = cacheLimit; + this.cachedFrames = cachedFramesQueue(cacheLimit); + } + + public Mono saveFrames(Flux frames) { + return Mono.defer( + () -> { + MonoProcessor completed = MonoProcessor.create(); + frames + .doFinally(s -> completed.onComplete()) + .subscribe( + frame -> { + cachedFramesSize.incrementAndGet(); + upstreamFrameRefCnt.compareAndSet(0, frame.refCnt()); + cachedFrames.offer(frame.retain()); + if (cachedFramesSize.get() == cacheLimit) { + releaseFrame(); + } + }); + return completed; + }); + } + + @Override + public void releaseFrames(long remoteImpliedPos) { + long pos = position.get(); + logger.debug( + "{} Removing frames for local: {}, remote implied: {}", tag, pos, remoteImpliedPos); + long removeCount = Math.max(0, remoteImpliedPos - pos); + long processedCount = 0; + while (processedCount < removeCount) { + releaseFrame(); + processedCount++; + } + logger.debug("{} Removed frames. Current size: {}", tag, cachedFramesSize.get()); + } + + private void releaseFrame() { + ByteBuf content = cachedFrames.poll(); + cachedFramesSize.decrementAndGet(); + content.release(); + position.incrementAndGet(); + } + + @Override + public Flux resumeStream() { + return Flux.create( + s -> { + int size = cachedFramesSize.get(); + int refCnt = upstreamFrameRefCnt.get(); + logger.debug("{} Resuming stream size: {}", tag, size); + /*spsc queue has no iterator - iterating by consuming*/ + for (int i = 0; i < size; i++) { + ByteBuf frame = cachedFrames.poll(); + /*in the event of connection termination some frames + * are not released on DuplexConnection*/ + if (frame.refCnt() == refCnt) { + frame.retain(); + } + cachedFrames.offer(frame); + s.next(frame); + } + s.complete(); + logger.debug("{} Resuming stream completed", tag); + }); + } + + @Override + public long framePosition() { + return position.get(); + } + + @Override + public long frameImpliedPosition() { + return impliedPosition.get(); + } + + @Override + public void resumableFrameReceived() { + impliedPosition.incrementAndGet(); + } + + @Override + public Mono onClose() { + return disposed; + } + + @Override + public void dispose() { + cachedFramesSize.set(0); + ByteBuf frame = cachedFrames.poll(); + while (frame != null) { + frame.release(); + frame = cachedFrames.poll(); + } + disposed.onComplete(); + } + + private static Queue cachedFramesQueue(int size) { + return Queues.get(size).get(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/PeriodicResumeStrategy.java b/rsocket-core/src/main/java/io/rsocket/resume/PeriodicResumeStrategy.java new file mode 100644 index 000000000..abfefe0b1 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/PeriodicResumeStrategy.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015-2019 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.resume; + +import java.time.Duration; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Mono; + +public class PeriodicResumeStrategy implements ResumeStrategy { + private final Duration interval; + + public PeriodicResumeStrategy(Duration interval) { + this.interval = interval; + } + + @Override + public Publisher apply(ClientResume clientResumeConfiguration, Throwable throwable) { + return Mono.delay(interval).thenReturn(toString()); + } + + @Override + public String toString() { + return "PeriodicResumeStrategy{" + "interval=" + interval + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java new file mode 100644 index 000000000..fc9261b99 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2019 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.resume; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Closeable; +import io.rsocket.DuplexConnection; +import reactor.core.publisher.Mono; + +public interface RSocketSession extends Closeable { + + ResumeToken token(); + + DuplexConnection resumableConnection(); + + RSocketSession continueWith(T ResumeAwareConnectionFactory); + + RSocketSession resumeWith(ByteBuf resumeFrame); + + void reconnect(ResumeAwareConnection connection); + + @Override + default Mono onClose() { + return resumableConnection().onClose(); + } + + @Override + default void dispose() { + resumableConnection().dispose(); + } + + @Override + default boolean isDisposed() { + return resumableConnection().isDisposed(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java b/rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java new file mode 100644 index 000000000..6553e5ec5 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/RequestListener.java @@ -0,0 +1,32 @@ +/* + * Copyright 2015-2019 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.resume; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.ReplayProcessor; + +class RequestListener { + private final ReplayProcessor requests = ReplayProcessor.create(1); + + public Flux apply(Flux flux) { + return flux.doOnRequest(requests::onNext); + } + + public Flux requests() { + return requests; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java new file mode 100644 index 000000000..a4bd9eb53 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -0,0 +1,376 @@ +/* + * Copyright 2015-2019 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.resume; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Closeable; +import io.rsocket.DuplexConnection; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.internal.UnboundedProcessor; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.publisher.*; + +class ResumableDuplexConnection implements DuplexConnection, ResumeStateHolder { + private static final Logger logger = LoggerFactory.getLogger(ResumableDuplexConnection.class); + + private final String tag; + private final ResumedFramesCalculator resumedFramesCalculator; + private final ResumableFramesStore resumableFramesStore; + private final Duration resumeStreamTimeout; + + private final ReplayProcessor connections = ReplayProcessor.create(1); + private final EmitterProcessor connectionErrors = EmitterProcessor.create(); + private volatile ResumeAwareConnection curConnection; + /*used instead of EmitterProcessor because its autocancel=false capability had no expected effect*/ + private final FluxProcessor downStreamFrames = ReplayProcessor.create(0); + private final FluxProcessor resumeSaveFrames = EmitterProcessor.create(); + private final MonoProcessor resumeSaveCompleted = MonoProcessor.create(); + + private final UnboundedProcessor actions = new UnboundedProcessor<>(); + + private final Mono framesSent; + private final RequestListener downStreamRequestListener = new RequestListener(); + private final RequestListener resumeSaveStreamRequestListener = new RequestListener(); + private final UnicastProcessor> upstreams = UnicastProcessor.create(); + private final UpstreamFramesSubscriber upstreamSubscriber = + new UpstreamFramesSubscriber( + 128, + downStreamRequestListener.requests(), + resumeSaveStreamRequestListener.requests(), + actions::onNext); + + private volatile State state; + private volatile Disposable impliedPosDisposable = Disposables.disposed(); + private volatile Disposable resumedStreamDisposable = Disposables.disposed(); + private final AtomicBoolean disposed = new AtomicBoolean(); + + ResumableDuplexConnection( + String tag, + ResumeAwareConnection duplexConnection, + ResumedFramesCalculator resumedFramesCalculator, + ResumableFramesStore resumableFramesStore, + Duration resumeStreamTimeout) { + this.tag = tag; + this.resumedFramesCalculator = resumedFramesCalculator; + this.resumableFramesStore = resumableFramesStore; + this.resumeStreamTimeout = resumeStreamTimeout; + + resumableFramesStore + .saveFrames(resumeSaveStreamRequestListener.apply(resumeSaveFrames)) + .subscribe(resumeSaveCompleted); + + upstreams.flatMap(Function.identity()).subscribe(upstreamSubscriber); + + framesSent = + connections + .switchMap( + c -> { + logger.debug("Switching transport: {}", tag); + return c.send(downStreamRequestListener.apply(downStreamFrames)) + .doFinally( + s -> + logger.debug( + "{} Transport send completed: {}, {}", tag, s, c.toString())) + .onErrorResume(err -> Mono.never()); + }) + .then() + .cache(); + + Flux acts = actions.publish().autoConnect(4); + acts.ofType(ByteBuf.class).subscribe(this::sendFrame); + acts.ofType(ResumeStart.class).subscribe(ResumeStart::run); + acts.ofType(Resume.class).subscribe(Resume::run); + acts.ofType(ResumeComplete.class).subscribe(ResumeComplete::run); + + reconnect(duplexConnection); + } + + /*reconnected by session after error. After this downstream can receive frames, + * but sending in suppressed until resume() is called*/ + public void reconnect(ResumeAwareConnection connection) { + if (curConnection == null) { + logger.debug("{} Resumable duplex connection started with connection: {}", tag, connection); + state = State.CONNECTED; + onNewConnection(connection); + } else { + logger.debug( + "{} Resumable duplex connection reconnected with connection: {}", tag, connection); + /*race between sendFrame and doResumeStart may lead to ongoing upstream frames + written before resume complete*/ + actions.onNext(new ResumeStart(connection)); + } + } + + /*after receiving RESUME (Server) or RESUME_OK (Client) + calculate and send resume frames */ + public void resume( + ResumptionState peerResumptionState, Function, Mono> resumeFrameSent) { + /*race between sendFrame and doResume may lead to duplicate frames on resume store*/ + actions.onNext(new Resume(peerResumptionState, resumeFrameSent)); + } + + @Override + public Mono sendOne(ByteBuf frame) { + return curConnection.sendOne(frame); + } + + @Override + public Mono send(Publisher frames) { + return Mono.defer( + () -> { + upstreams.onNext(Flux.from(frames)); + return framesSent; + }); + } + + @Override + public Flux receive() { + return connections.switchMap( + c -> + c.receive() + .doOnNext( + f -> { + if (isResumableFrame(f)) { + resumableFramesStore.resumableFrameReceived(); + } + }) + .onErrorResume(err -> Mono.never())); + } + + @Override + public long impliedPosition() { + return resumableFramesStore.frameImpliedPosition(); + } + + @Override + public Mono onClose() { + return Flux.merge(connections.last().flatMap(Closeable::onClose), resumeSaveCompleted).then(); + } + + @Override + public void dispose() { + if (disposed.compareAndSet(false, true)) { + logger.debug("Resumable connection disposed: {}, {}", tag, this); + upstreams.onComplete(); + connections.onComplete(); + connectionErrors.onComplete(); + resumeSaveFrames.onComplete(); + curConnection.dispose(); + upstreamSubscriber.dispose(); + impliedPosDisposable.dispose(); + resumedStreamDisposable.dispose(); + resumableFramesStore.dispose(); + } + } + + @Override + public double availability() { + return curConnection.availability(); + } + + @Override + public boolean isDisposed() { + return disposed.get(); + } + + private void acceptRemoteResumePositions() { + impliedPosDisposable.dispose(); + impliedPosDisposable = + curConnection + .receiveResumePositions(this) + .doOnNext(l -> logger.debug("Got remote position from keep-alive: {}", l)) + .subscribe(this::releaseFramesToPosition); + } + + private void sendFrame(ByteBuf f) { + /*resuming from store so no need to save again*/ + if (isResumableFrame(f) && state != State.RESUME) { + resumeSaveFrames.onNext(f); + } + /*filter frames coming from upstream before actual resumption began, + * to preserve frames ordering*/ + if (state != State.RESUME_STARTED) { + downStreamFrames.onNext(f); + } + } + + ResumptionState state() { + return new ResumptionState( + resumableFramesStore.framePosition(), resumableFramesStore.frameImpliedPosition()); + } + + Flux connectionErrors() { + return connectionErrors; + } + + private void doResumeStart(ResumeAwareConnection connection) { + state = State.RESUME_STARTED; + resumedStreamDisposable.dispose(); + upstreamSubscriber.resumeStart(); + onNewConnection(connection); + } + + private void doResume( + ResumptionState peerResumptionState, Function, Mono> sendResumeFrame) { + ResumptionState localResumptionState = state(); + + logger.debug( + "Resumption start. Calculating implied pos using: {}", + resumedFramesCalculator.getClass().getSimpleName()); + logger.debug( + "Resumption states. local: {}, remote: {}", localResumptionState, peerResumptionState); + + Mono res = resumedFramesCalculator.calculate(localResumptionState, peerResumptionState); + Mono localImpliedPos = + res.doOnSuccess(notUsed -> state = State.RESUME) + .doOnSuccess(this::releaseFramesToPosition) + .map(remoteImpliedPos -> localResumptionState.impliedPosition()); + + sendResumeFrame + .apply(localImpliedPos) + .then( + streamResumedFrames( + resumableFramesStore + .resumeStream() + .timeout(resumeStreamTimeout) + .doFinally(s -> actions.onNext(new ResumeComplete()))) + .doOnError(err -> dispose())) + .onErrorResume(err -> Mono.empty()) + .subscribe(); + } + + private void doResumeComplete() { + logger.debug("Completing resumption"); + state = State.RESUME_COMPLETED; + upstreamSubscriber.resumeComplete(); + acceptRemoteResumePositions(); + } + + private Mono streamResumedFrames(Flux frames) { + return Mono.create( + s -> { + ResumeFramesSubscriber subscriber = + new ResumeFramesSubscriber( + downStreamRequestListener.requests(), actions::onNext, s::error, s::success); + s.onDispose(subscriber); + resumedStreamDisposable = subscriber; + frames.subscribe(subscriber); + }); + } + + private void onNewConnection(ResumeAwareConnection connection) { + curConnection = connection; + connection.onClose().doFinally(v -> disconnect(connection)).subscribe(); + connections.onNext(connection); + } + + private void disconnect(DuplexConnection connection) { + /*do not report late disconnects on old connection if new one is available*/ + if (curConnection == connection && state.isActive()) { + Throwable err = new ClosedChannelException(); + state = State.DISCONNECTED; + logger.debug("{} Inner connection disconnected: {}", tag, err.getClass().getSimpleName()); + connectionErrors.onNext(err); + } + } + + /*remove frames confirmed by implied pos, + set current pos accordingly*/ + private void releaseFramesToPosition(Long remoteImpliedPos) { + resumableFramesStore.releaseFrames(remoteImpliedPos); + } + + static boolean isResumableFrame(ByteBuf frame) { + switch (FrameHeaderFlyweight.frameType(frame)) { + case REQUEST_CHANNEL: + case REQUEST_STREAM: + case REQUEST_RESPONSE: + case REQUEST_FNF: + case REQUEST_N: + case CANCEL: + case ERROR: + case NEXT: + case NEXT_COMPLETE: + return true; + default: + return false; + } + } + + private enum State { + CONNECTED(true), + RESUME_STARTED(true), + RESUME(true), + RESUME_COMPLETED(true), + DISCONNECTED(false); + + private final boolean active; + + State(boolean active) { + this.active = active; + } + + public boolean isActive() { + return active; + } + } + + class ResumeStart implements Runnable { + private ResumeAwareConnection connection; + + public ResumeStart(ResumeAwareConnection connection) { + this.connection = connection; + } + + @Override + public void run() { + doResumeStart(connection); + } + } + + class Resume implements Runnable { + private final ResumptionState peerResumptionState; + private final Function, Mono> resumeFrameSent; + + public Resume( + ResumptionState peerResumptionState, Function, Mono> resumeFrameSent) { + this.peerResumptionState = peerResumptionState; + this.resumeFrameSent = resumeFrameSent; + } + + @Override + public void run() { + doResume(peerResumptionState, resumeFrameSent); + } + } + + private class ResumeComplete implements Runnable { + + @Override + public void run() { + doResumeComplete(); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java new file mode 100644 index 000000000..849a78dd4 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java @@ -0,0 +1,55 @@ +/* + * Copyright 2015-2019 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.resume; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Closeable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** Store for resumable frames */ +public interface ResumableFramesStore extends Closeable { + + /** + * Save resumable frames for potential resumption + * + * @param frames {@link Flux} of resumable frames + * @return {@link Mono} which completes once all resume frames are written + */ + Mono saveFrames(Flux frames); + + /** Release frames from tail of the store up to remote implied position */ + void releaseFrames(long remoteImpliedPos); + + /** + * @return {@link Flux} of frames from store tail to head. It should terminate with error if + * frames are not continuous + */ + Flux resumeStream(); + + /** @return Local frame position as defined by RSocket protocol */ + long framePosition(); + + /** @return Implied frame position as defined by RSocket protocol */ + long frameImpliedPosition(); + + /** + * Received resumable frame as defined by RSocket protocol. Implementation must increment frame + * implied position + */ + void resumableFrameReceived(); +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeAwareConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeAwareConnection.java new file mode 100644 index 000000000..fe6baab4a --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeAwareConnection.java @@ -0,0 +1,25 @@ +/* + * Copyright 2015-2019 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.resume; + +import io.rsocket.DuplexConnection; +import reactor.core.publisher.Flux; + +public interface ResumeAwareConnection extends DuplexConnection { + + Flux receiveResumePositions(ResumeStateHolder resumeStateHolder); +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeCache.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeCache.java deleted file mode 100644 index f85949c60..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumeCache.java +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2015-2018 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.resume; - -import io.netty.buffer.ByteBuf; -import java.util.*; -import reactor.core.publisher.Flux; - -public class ResumeCache { - private final ResumePositionCounter strategy; - private final int maxBufferSize; - - private final LinkedHashMap frames = new LinkedHashMap<>(); - private int lastRemotePosition = 0; - private int currentPosition = 0; - private int bufferSize; - - public ResumeCache(ResumePositionCounter strategy, int maxBufferSize) { - this.strategy = strategy; - this.maxBufferSize = maxBufferSize; - } - - public void updateRemotePosition(int remotePosition) { - if (remotePosition > currentPosition) { - throw new IllegalStateException( - "Remote ahead of " + lastRemotePosition + " , expected " + remotePosition); - } - - if (remotePosition == lastRemotePosition) { - return; - } - - if (remotePosition < lastRemotePosition) { - throw new IllegalStateException( - "Remote position moved back from " + lastRemotePosition + " to " + remotePosition); - } - - lastRemotePosition = remotePosition; - - Iterator> positions = frames.entrySet().iterator(); - - while (positions.hasNext()) { - Map.Entry cachePosition = positions.next(); - - if (cachePosition.getKey() <= remotePosition) { - positions.remove(); - bufferSize -= strategy.cost(cachePosition.getValue()); - cachePosition.getValue().release(); - } - - // TODO check for a bad position - } - } - - public void sent(ByteBuf frame) { - if (ResumeUtil.isTracked(frame)) { - frames.put(currentPosition, frame.copy()); - bufferSize += strategy.cost(frame); - - currentPosition += ResumeUtil.offset(frame); - - if (frames.size() > maxBufferSize) { - ByteBuf f = frames.remove(first(frames)); - bufferSize -= strategy.cost(f); - } - } - } - - private int first(LinkedHashMap frames) { - return frames.keySet().iterator().next(); - } - - public Flux resend(int remotePosition) { - updateRemotePosition(remotePosition); - - if (remotePosition == currentPosition) { - return Flux.empty(); - } - - List resend = new ArrayList<>(); - - for (Map.Entry cachePosition : frames.entrySet()) { - if (remotePosition < cachePosition.getKey()) { - resend.add(cachePosition.getValue()); - } - - // TODO error handling - } - - return Flux.fromIterable(resend); - } - - public int getCurrentPosition() { - return currentPosition; - } - - public int getRemotePosition() { - return lastRemotePosition; - } - - public int getEarliestResendPosition() { - if (frames.isEmpty()) { - return currentPosition; - } else { - return first(frames); - } - } - - public int size() { - return bufferSize; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeFramesSubscriber.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeFramesSubscriber.java new file mode 100644 index 000000000..4facdd3c1 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeFramesSubscriber.java @@ -0,0 +1,88 @@ +/* + * Copyright 2015-2019 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.resume; + +import io.netty.buffer.ByteBuf; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; + +class ResumeFramesSubscriber implements Subscriber, Disposable { + private final Flux requests; + private final Consumer onNext; + private final Consumer onError; + private final Runnable onComplete; + private final AtomicBoolean disposed = new AtomicBoolean(); + private volatile Disposable requestsDisposable; + private volatile Subscription subscription; + + public ResumeFramesSubscriber( + Flux requests, + Consumer onNext, + Consumer onError, + Runnable onComplete) { + this.requests = requests; + this.onNext = onNext; + this.onError = onError; + this.onComplete = onComplete; + } + + @Override + public void onSubscribe(Subscription s) { + if (isDisposed()) { + s.cancel(); + } else { + this.subscription = s; + this.requestsDisposable = requests.subscribe(s::request); + } + } + + @Override + public void onNext(ByteBuf frame) { + this.onNext.accept(frame); + } + + @Override + public void onError(Throwable t) { + this.onError.accept(t); + requestsDisposable.dispose(); + } + + @Override + public void onComplete() { + this.onComplete.run(); + requestsDisposable.dispose(); + } + + @Override + public void dispose() { + if (disposed.compareAndSet(false, true)) { + if (subscription != null) { + subscription.cancel(); + requestsDisposable.dispose(); + } + } + } + + @Override + public boolean isDisposed() { + return disposed.get(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateException.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateException.java new file mode 100644 index 000000000..173e62bb0 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateException.java @@ -0,0 +1,36 @@ +/* + * Copyright 2015-2019 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.resume; + +class ResumeStateException extends RuntimeException { + private static final long serialVersionUID = -5393753463377588732L; + private final ResumptionState local; + private final ResumptionState remote; + + public ResumeStateException(ResumptionState local, ResumptionState remote) { + this.local = local; + this.remote = remote; + } + + public ResumptionState localState() { + return local; + } + + public ResumptionState remoteState() { + return remote; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java new file mode 100644 index 000000000..9ab62b002 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java @@ -0,0 +1,22 @@ +/* + * Copyright 2015-2019 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.resume; + +public interface ResumeStateHolder { + + long impliedPosition(); +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java new file mode 100644 index 000000000..903431192 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java @@ -0,0 +1,23 @@ +/* + * Copyright 2015-2019 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.resume; + +import java.util.function.BiFunction; +import org.reactivestreams.Publisher; + +@FunctionalInterface +public interface ResumeStrategy extends BiFunction> {} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeToken.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeToken.java index 8f33f7951..64d6d51f4 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumeToken.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeToken.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2019 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. @@ -16,21 +16,31 @@ package io.rsocket.resume; +import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; import java.nio.ByteBuffer; import java.util.Arrays; +import java.util.Objects; import java.util.UUID; +import javax.annotation.Nullable; public final class ResumeToken { - // TODO consider best format to store this + private static final int MAX_TOKEN_LENGTH = 65535; // unsigned 16 bit + private static final ResumeToken EMPTY_TOKEN = new ResumeToken(new byte[0]); private final byte[] resumeToken; protected ResumeToken(byte[] resumeToken) { + assertToken(resumeToken); this.resumeToken = resumeToken; } - public static ResumeToken bytes(byte[] token) { - return new ResumeToken(token); + public static ResumeToken fromBytes(@Nullable byte[] token) { + return token == null || token.length == 0 ? EMPTY_TOKEN : new ResumeToken(token); + } + + public static ResumeToken empty() { + return EMPTY_TOKEN; } public static ResumeToken generate() { @@ -55,7 +65,6 @@ public boolean equals(Object obj) { if (obj instanceof ResumeToken) { return Arrays.equals(resumeToken, ((ResumeToken) obj).resumeToken); } - return false; } @@ -64,7 +73,29 @@ public String toString() { return ByteBufUtil.hexDump(resumeToken); } + public int length() { + return resumeToken.length; + } + + public boolean isEmpty() { + return length() == 0; + } + public byte[] toByteArray() { return resumeToken; } + + public ByteBuf toByteBuf() { + return isEmpty() ? Unpooled.EMPTY_BUFFER : Unpooled.wrappedBuffer(toByteArray()); + } + + private static void assertToken(byte[] resumeToken) { + Objects.requireNonNull(resumeToken); + int tokenLength = resumeToken.length; + if (tokenLength > MAX_TOKEN_LENGTH) { + throw new IllegalArgumentException( + String.format( + "Resumption token length exceeds limit of %d: %d", MAX_TOKEN_LENGTH, tokenLength)); + } + } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeUtil.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeUtil.java deleted file mode 100644 index 36558d7ce..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumeUtil.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2015-2018 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.resume; - -import io.netty.buffer.ByteBuf; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.FrameType; - -public class ResumeUtil { - public static boolean isTracked(FrameType frameType) { - switch (frameType) { - case REQUEST_CHANNEL: - case REQUEST_STREAM: - case REQUEST_RESPONSE: - case REQUEST_FNF: - // case METADATA_PUSH: - case REQUEST_N: - case CANCEL: - case ERROR: - case PAYLOAD: - return true; - default: - return false; - } - } - - public static boolean isTracked(ByteBuf frame) { - return isTracked(FrameHeaderFlyweight.frameType(frame)); - } - - public static int offset(ByteBuf frame) { - return 0; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumedFramesCalculator.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumedFramesCalculator.java new file mode 100644 index 000000000..304470adf --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumedFramesCalculator.java @@ -0,0 +1,52 @@ +/* + * Copyright 2015-2019 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.resume; + +import reactor.core.publisher.Mono; + +interface ResumedFramesCalculator { + + ResumedFramesCalculator ofClient = new ClientResumedFramesCalculator(); + ResumedFramesCalculator ofServer = new ServerResumedFramesCalculator(); + + Mono calculate(ResumptionState local, ResumptionState remote); + + class ClientResumedFramesCalculator implements ResumedFramesCalculator { + + @Override + public Mono calculate(ResumptionState clientState, ResumptionState serverState) { + long serverImplied = serverState.impliedPosition(); + if (serverImplied >= clientState.position()) { + return Mono.just(serverImplied); + } else { + return Mono.error(new ResumeStateException(clientState, serverState)); + } + } + } + + class ServerResumedFramesCalculator implements ResumedFramesCalculator { + + @Override + public Mono calculate(ResumptionState serverState, ResumptionState clientState) { + boolean clientStateValid = clientState.position() <= serverState.impliedPosition(); + boolean serverStateValid = serverState.position() <= clientState.impliedPosition(); + return clientStateValid && serverStateValid + ? Mono.just(clientState.impliedPosition()) + : Mono.error(new ResumeStateException(serverState, clientState)); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumptionState.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumptionState.java new file mode 100644 index 000000000..4ca1bcc1f --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumptionState.java @@ -0,0 +1,67 @@ +/* + * Copyright 2015-2019 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.resume; + +import java.util.Objects; + +class ResumptionState { + private final long pos; + private final long impliedPos; + + ResumptionState(long pos, long impliedPos) { + this.pos = pos; + this.impliedPos = impliedPos; + } + + public static ResumptionState fromServer(long impliedPos) { + return new ResumptionState(-1, impliedPos); + } + + public static ResumptionState fromClient(long pos, long impliedPos) { + return new ResumptionState(pos, impliedPos); + } + + public boolean isServer() { + return pos < 0; + } + + public long position() { + return pos; + } + + public long impliedPosition() { + return impliedPos; + } + + @Override + public String toString() { + return "ResumptionState{" + "pos=" + pos + ", impliedPos=" + impliedPos + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ResumptionState that = (ResumptionState) o; + return pos == that.pos && impliedPos == that.impliedPos; + } + + @Override + public int hashCode() { + return Objects.hash(pos, impliedPos); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java new file mode 100644 index 000000000..982513aa1 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -0,0 +1,158 @@ +/* + * Copyright 2015-2019 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.resume; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.exceptions.RejectedResumeException; +import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ResumeFrameFlyweight; +import io.rsocket.frame.ResumeOkFrameFlyweight; +import io.rsocket.internal.KeepAliveData; +import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.FluxProcessor; +import reactor.core.publisher.Mono; +import reactor.core.publisher.ReplayProcessor; + +public class ServerRSocketSession implements RSocketSession { + private static final Logger logger = LoggerFactory.getLogger(ServerRSocketSession.class); + + private final ResumableDuplexConnection resumableConnection; + /*used instead of EmitterProcessor because its autocancel=false capability had no expected effect*/ + private final FluxProcessor newConnections = + ReplayProcessor.create(0); + private final ByteBufAllocator allocator; + private final KeepAliveData keepAliveData; + private final ResumeToken resumeToken; + + public ServerRSocketSession( + ByteBufAllocator allocator, + ResumeAwareConnection duplexConnection, + ServerResumeConfiguration config, + KeepAliveData keepAliveData, + ResumeToken resumeToken) { + this.allocator = Objects.requireNonNull(allocator); + this.keepAliveData = Objects.requireNonNull(keepAliveData); + this.resumeToken = Objects.requireNonNull(resumeToken); + this.resumableConnection = + new ResumableDuplexConnection( + "server", + duplexConnection, + ResumedFramesCalculator.ofServer, + config.resumeStoreFactory().apply(resumeToken), + config.resumeStreamTimeout()); + + Mono timeout = + resumableConnection + .connectionErrors() + .flatMap( + err -> { + logger.debug("Starting session timeout due to error: {}", err); + return newConnections + .next() + .doOnNext(c -> logger.debug("Connection after error: {}", c)) + .timeout(config.sessionDuration()); + }) + .then() + .cast(ResumeAwareConnection.class); + + newConnections + .mergeWith(timeout) + .subscribe( + connection -> { + reconnect(connection); + logger.debug("Server ResumableConnection reconnected: {}", connection); + }, + err -> { + logger.debug("Server ResumableConnection reconnect timeout"); + resumableConnection.dispose(); + }); + } + + @Override + public ServerRSocketSession continueWith(ResumeAwareConnection newConnection) { + logger.debug("Server continued with connection: {}", newConnection); + newConnections.onNext(newConnection); + return this; + } + + @Override + public ServerRSocketSession resumeWith(ByteBuf resumeFrame) { + logger.debug("Resume FRAME received"); + resumableConnection.resume( + stateFromFrame(resumeFrame), + pos -> + pos.flatMap( + impliedPos -> sendFrame(ResumeOkFrameFlyweight.encode(allocator, impliedPos))) + .onErrorResume( + err -> + sendFrame( + ErrorFrameFlyweight.encode(allocator, 0, errorFrameThrowable(err))) + .then(Mono.fromRunnable(resumableConnection::dispose)) + /*Resumption is impossible: no need to return control to ResumableConnection*/ + .then(Mono.never()))); + return this; + } + + @Override + public void reconnect(ResumeAwareConnection connection) { + resumableConnection.reconnect(connection); + } + + @Override + public DuplexConnection resumableConnection() { + return resumableConnection; + } + + @Override + public ResumeToken token() { + return resumeToken; + } + + public KeepAliveData keepAliveData() { + return keepAliveData; + } + + private Mono sendFrame(ByteBuf frame) { + logger.debug("Sending Resume frame: {}", frame); + return resumableConnection.sendOne(frame).onErrorResume(e -> Mono.empty()); + } + + private static ResumptionState stateFromFrame(ByteBuf resumeFrame) { + long peerPos = ResumeFrameFlyweight.firstAvailableClientPos(resumeFrame); + long peerImpliedPos = ResumeFrameFlyweight.lastReceivedServerPos(resumeFrame); + resumeFrame.release(); + return ResumptionState.fromClient(peerPos, peerImpliedPos); + } + + private static RejectedResumeException errorFrameThrowable(Throwable err) { + String msg; + if (err instanceof ResumeStateException) { + ResumeStateException resumeException = ((ResumeStateException) err); + msg = + String.format( + "resumption_pos=[ remote: %s, local: %s]", + resumeException.remoteState(), resumeException.localState()); + } else { + msg = String.format("resume_internal_error: %s", err.getMessage()); + } + return new RejectedResumeException(msg); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerResumeConfiguration.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerResumeConfiguration.java new file mode 100644 index 000000000..db291309f --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerResumeConfiguration.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015-2019 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.resume; + +import java.time.Duration; +import java.util.function.Function; + +public class ServerResumeConfiguration { + private final Duration sessionDuration; + private final Duration resumeStreamTimeout; + private final Function resumeStoreFactory; + + public ServerResumeConfiguration( + Duration sessionDuration, + Duration resumeStreamTimeout, + Function resumeStoreFactory) { + this.sessionDuration = sessionDuration; + this.resumeStreamTimeout = resumeStreamTimeout; + this.resumeStoreFactory = resumeStoreFactory; + } + + public Duration sessionDuration() { + return sessionDuration; + } + + public Duration resumeStreamTimeout() { + return resumeStreamTimeout; + } + + public Function resumeStoreFactory() { + return resumeStoreFactory; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java b/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java new file mode 100644 index 000000000..c8c179dcf --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2019 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.resume; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class SessionManager { + private boolean isDisposed; + private final Map sessions = new ConcurrentHashMap<>(); + + public ServerRSocketSession save(ServerRSocketSession session) { + if (isDisposed) { + session.dispose(); + } else { + ResumeToken token = session.token(); + session.onClose().doOnSuccess(v -> sessions.remove(token)).subscribe(); + ServerRSocketSession prev = sessions.put(token, session); + if (prev != null) { + prev.dispose(); + } + } + return session; + } + + public Optional get(ResumeToken resumeToken) { + return Optional.ofNullable(sessions.get(resumeToken)); + } + + public void dispose() { + isDisposed = true; + sessions.values().forEach(ServerRSocketSession::dispose); + sessions.clear(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java b/rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java new file mode 100644 index 000000000..377ae9761 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java @@ -0,0 +1,190 @@ +/* + * Copyright 2015-2019 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.resume; + +import io.netty.buffer.ByteBuf; +import io.rsocket.internal.UnboundedProcessor; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Operators; +import reactor.util.concurrent.Queues; + +class UpstreamFramesSubscriber implements Subscriber, Disposable { + private static final Logger logger = LoggerFactory.getLogger(UpstreamFramesSubscriber.class); + + private final UnboundedProcessor actions = new UnboundedProcessor<>(); + private final AtomicBoolean disposed = new AtomicBoolean(); + private final Consumer itemConsumer; + private final Disposable downstreamRequestDisposable; + private final Disposable resumeSaveStreamDisposable; + + private volatile Subscription subs; + private volatile boolean resumeStarted; + private final Queue framesCache; + private long request; + private long downStreamRequestN; + private long resumeSaveStreamRequestN; + + UpstreamFramesSubscriber( + int estimatedDownstreamRequest, + Flux downstreamRequests, + Flux resumeSaveStreamRequests, + Consumer itemConsumer) { + this.itemConsumer = itemConsumer; + this.framesCache = Queues.unbounded(estimatedDownstreamRequest).get(); + + downstreamRequestDisposable = downstreamRequests.subscribe(requestN -> requestN(0, requestN)); + + resumeSaveStreamDisposable = + resumeSaveStreamRequests.subscribe(requestN -> requestN(requestN, 0)); + + Flux acts = actions.publish().autoConnect(3); + acts.ofType(ByteBuf.class).subscribe(this::processFrame); + acts.ofType(ResumeStart.class).subscribe(ResumeStart::run); + acts.ofType(ResumeComplete.class).subscribe(ResumeComplete::run); + } + + @Override + public void onSubscribe(Subscription s) { + this.subs = s; + if (!isDisposed()) { + doRequest(); + } else { + s.cancel(); + } + } + + @Override + public void onNext(ByteBuf item) { + actions.onNext(item); + } + + @Override + public void onError(Throwable t) { + dispose(); + } + + @Override + public void onComplete() { + dispose(); + } + + public void resumeStart() { + actions.onNext(new ResumeStart()); + } + + public void resumeComplete() { + actions.onNext(new ResumeComplete()); + } + + @Override + public void dispose() { + if (disposed.compareAndSet(false, true)) { + releaseCache(); + if (subs != null) { + subs.cancel(); + } + resumeSaveStreamDisposable.dispose(); + downstreamRequestDisposable.dispose(); + } + } + + @Override + public boolean isDisposed() { + return disposed.get(); + } + + private void requestN(long resumeStreamRequest, long downStreamRequest) { + synchronized (this) { + downStreamRequestN = Operators.addCap(downStreamRequestN, downStreamRequest); + resumeSaveStreamRequestN = Operators.addCap(resumeSaveStreamRequestN, resumeStreamRequest); + + long requests = Math.min(downStreamRequestN, resumeSaveStreamRequestN); + if (requests > 0) { + downStreamRequestN -= requests; + resumeSaveStreamRequestN -= requests; + logger.debug("Upstream subscriber requestN: {}", requests); + request = Operators.addCap(request, requests); + } + } + doRequest(); + } + + private void doRequest() { + if (subs != null && !resumeStarted) { + synchronized (this) { + long r = request; + if (r > 0) { + subs.request(r); + request = 0; + } + } + } + } + + private void releaseCache() { + ByteBuf frame = framesCache.poll(); + while (frame != null && frame.refCnt() > 0) { + frame.release(); + } + } + + private void doResumeStart() { + resumeStarted = true; + } + + private void doResumeComplete() { + ByteBuf frame = framesCache.poll(); + while (frame != null) { + itemConsumer.accept(frame); + frame = framesCache.poll(); + } + resumeStarted = false; + doRequest(); + } + + private void processFrame(ByteBuf item) { + if (resumeStarted) { + framesCache.offer(item); + } else { + itemConsumer.accept(item); + } + } + + private class ResumeStart implements Runnable { + + @Override + public void run() { + doResumeStart(); + } + } + + private class ResumeComplete implements Runnable { + + @Override + public void run() { + doResumeComplete(); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumePositionCounter.java b/rsocket-core/src/main/java/io/rsocket/resume/package-info.java similarity index 61% rename from rsocket-core/src/main/java/io/rsocket/resume/ResumePositionCounter.java rename to rsocket-core/src/main/java/io/rsocket/resume/package-info.java index 273058731..57027bee2 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumePositionCounter.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/package-info.java @@ -14,22 +14,5 @@ * limitations under the License. */ +@javax.annotation.ParametersAreNonnullByDefault package io.rsocket.resume; - -import io.netty.buffer.ByteBuf; - -/** - * Calculates the cost of a Frame when stored in the ResumeCache. Two obvious and provided - * strategies are simple frame counts and size in bytes. - */ -public interface ResumePositionCounter { - int cost(ByteBuf f); - - static ResumePositionCounter size() { - return ResumeUtil::offset; - } - - static ResumePositionCounter frames() { - return f -> 1; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/util/ConnectionUtils.java b/rsocket-core/src/main/java/io/rsocket/util/ConnectionUtils.java new file mode 100644 index 000000000..dd8bbf907 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/ConnectionUtils.java @@ -0,0 +1,17 @@ +package io.rsocket.util; + +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.internal.ClientServerInputMultiplexer; +import reactor.core.publisher.Mono; + +public class ConnectionUtils { + + public static Mono sendError( + ByteBufAllocator allocator, ClientServerInputMultiplexer multiplexer, Exception exception) { + return multiplexer + .asSetupConnection() + .sendOne(ErrorFrameFlyweight.encode(allocator, 0, exception)) + .onErrorResume(err -> Mono.empty()); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/util/DuplexConnectionProxy.java b/rsocket-core/src/main/java/io/rsocket/util/DuplexConnectionProxy.java new file mode 100644 index 000000000..fa19553a7 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/DuplexConnectionProxy.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2019 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.util; + +import io.netty.buffer.ByteBuf; +import io.rsocket.DuplexConnection; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class DuplexConnectionProxy implements DuplexConnection { + private final DuplexConnection connection; + + public DuplexConnectionProxy(DuplexConnection connection) { + this.connection = connection; + } + + @Override + public Mono send(Publisher frames) { + return connection.send(frames); + } + + @Override + public Flux receive() { + return connection.receive(); + } + + @Override + public double availability() { + return connection.availability(); + } + + @Override + public Mono onClose() { + return connection.onClose(); + } + + @Override + public void dispose() { + connection.dispose(); + } + + @Override + public boolean isDisposed() { + return connection.isDisposed(); + } + + public DuplexConnection delegate() { + return connection; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/util/Function3.java b/rsocket-core/src/main/java/io/rsocket/util/Function3.java new file mode 100644 index 000000000..5783665ae --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/Function3.java @@ -0,0 +1,22 @@ +/* + * Copyright 2015-2019 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.util; + +public interface Function3 { + + R apply(T t, U u, V v); +} diff --git a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java index 84e6a6a43..eb95464b4 100644 --- a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java @@ -1,172 +1,228 @@ +/* + * Copyright 2015-2019 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; +import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCountUtil; import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; import io.rsocket.frame.KeepAliveFrameFlyweight; +import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.internal.KeepAliveData; +import io.rsocket.keepalive.KeepAliveConnection; +import io.rsocket.resume.ResumeStateHolder; import io.rsocket.test.util.TestDuplexConnection; -import io.rsocket.util.DefaultPayload; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; import java.util.List; import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Stream; +import java.util.function.Function; +import java.util.stream.Collectors; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.test.StepVerifier; +import reactor.core.publisher.ReplayProcessor; public class KeepAliveTest { - private static final int CLIENT_REQUESTER_TICK_PERIOD = 100; - private static final int CLIENT_REQUESTER_TIMEOUT = 700; - private static final int CLIENT_REQUESTER_MISSED_ACKS = 3; - private static final int SERVER_RESPONDER_TICK_PERIOD = 100; - private static final int SERVER_RESPONDER_TIMEOUT = 1000; - - static Stream> testData() { - return Stream.of( - requester( - CLIENT_REQUESTER_TICK_PERIOD, CLIENT_REQUESTER_TIMEOUT, CLIENT_REQUESTER_MISSED_ACKS), - responder(SERVER_RESPONDER_TICK_PERIOD, SERVER_RESPONDER_TIMEOUT)); - } - - static Supplier requester(int tickPeriod, int timeout, int missedAcks) { - return () -> { - TestDuplexConnection connection = new TestDuplexConnection(); - Errors errors = new Errors(); - RSocketClient rSocket = - new RSocketClient( - ByteBufAllocator.DEFAULT, - connection, - DefaultPayload::create, - errors, - StreamIdSupplier.clientSupplier(), - Duration.ofMillis(tickPeriod), - Duration.ofMillis(timeout), - missedAcks); - return new TestData(rSocket, errors, connection); - }; + private static final int TICK_PERIOD = 100; + private static final int TIMEOUT = 700; + + private TestDuplexConnection testConnection; + private Function> timingsProvider; + private List errors; + private Consumer errorConsumer; + private KeepAliveConnection clientConnection; + private KeepAliveConnection serverConnection; + private ByteBufAllocator allocator; + + @BeforeEach + void setUp() { + allocator = ByteBufAllocator.DEFAULT; + testConnection = new TestDuplexConnection(); + + timingsProvider = f -> Mono.just(new KeepAliveData(TICK_PERIOD, TIMEOUT)); + errors = new ArrayList<>(); + errorConsumer = errors::add; + + clientConnection = + KeepAliveConnection.ofClient(allocator, testConnection, timingsProvider, errorConsumer); + serverConnection = + KeepAliveConnection.ofServer(allocator, testConnection, timingsProvider, errorConsumer); } - static Supplier responder(int tickPeriod, int timeout) { - return () -> { - TestDuplexConnection connection = new TestDuplexConnection(); - AbstractRSocket handler = new AbstractRSocket() {}; - Errors errors = new Errors(); - RSocketServer rSocket = - new RSocketServer( - ByteBufAllocator.DEFAULT, - connection, - handler, - DefaultPayload::create, - errors, - tickPeriod, - timeout); - return new TestData(rSocket, errors, connection); - }; - } + @Test + void clientNoFramesBeforeSetup() { - @ParameterizedTest - @MethodSource("testData") - void keepAlives(Supplier testDataSupplier) { - TestData testData = testDataSupplier.get(); - TestDuplexConnection connection = testData.connection(); + Mono.delay(Duration.ofSeconds(1)).block(); - Flux.interval(Duration.ofMillis(100)) - .subscribe( - n -> - connection.addToReceivedBuffer( - KeepAliveFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, true, 0, Unpooled.EMPTY_BUFFER))); + Assertions.assertThat(errors).isEmpty(); + Assertions.assertThat(clientConnection.isDisposed()).isFalse(); + Assertions.assertThat(clientConnection.availability()).isEqualTo(testConnection.availability()); + Assertions.assertThat(testConnection.getSent()).isEmpty(); + } - Mono.delay(Duration.ofMillis(1500)).block(); + @Test + void clientFramesAfterSetup() { + clientConnection.sendOne(setupFrame()).subscribe(); - RSocket rSocket = testData.rSocket(); - List errors = testData.errors().errors(); + Mono.delay(Duration.ofMillis(500)).block(); - Assertions.assertThat(rSocket.isDisposed()).isFalse(); Assertions.assertThat(errors).isEmpty(); + Assertions.assertThat(clientConnection.isDisposed()).isFalse(); + Collection sent = testConnection.getSent(); + Collection sentAfterSetup = + sent.stream().filter(f -> frameType(f) != FrameType.SETUP).collect(Collectors.toList()); + + Assertions.assertThat(sentAfterSetup).isNotEmpty(); + sentAfterSetup.forEach( + f -> { + Assertions.assertThat(frameType(f)).isEqualTo(FrameType.KEEPALIVE); + Assertions.assertThat(KeepAliveFrameFlyweight.respondFlag(f)).isEqualTo(true); + Assertions.assertThat(KeepAliveFrameFlyweight.lastPosition(f)).isEqualTo(0); + }); + + sent.forEach(ReferenceCountUtil::safeRelease); } - @ParameterizedTest - @MethodSource("testData") - void keepAlivesMissing(Supplier testDataSupplier) { - TestData testData = testDataSupplier.get(); - RSocket rSocket = testData.rSocket(); + @Test + void clientCloseOnMissingKeepalives() { + clientConnection.sendOne(setupFrame()).subscribe(); - Mono.delay(Duration.ofMillis(1500)).block(); + Mono.delay(Duration.ofSeconds(1)).block(); - List errors = testData.errors().errors(); - Assertions.assertThat(rSocket.isDisposed()).isTrue(); Assertions.assertThat(errors).hasSize(1); - Throwable throwable = errors.get(0); - Assertions.assertThat(throwable).isInstanceOf(ConnectionErrorException.class); + Throwable err = errors.get(0); + Assertions.assertThat(err).isExactlyInstanceOf(ConnectionErrorException.class); + Assertions.assertThat(err.getMessage()).isEqualTo("No keep-alive acks for 700 ms"); + Assertions.assertThat(clientConnection.isDisposed()).isTrue(); + + testConnection.getSent().forEach(ReferenceCountUtil::safeRelease); } @Test - void clientRequesterRespondsToKeepAlives() { - TestData testData = requester(100, 700, 3).get(); - TestDuplexConnection connection = testData.connection(); - - Mono.delay(Duration.ofMillis(100)) - .subscribe( - l -> - connection.addToReceivedBuffer( - KeepAliveFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, true, 0, Unpooled.EMPTY_BUFFER))); - - Mono keepAliveResponse = - Flux.from(connection.getSentAsPublisher()) - .filter( - f -> - FrameHeaderFlyweight.frameType(f) == FrameType.KEEPALIVE - && !KeepAliveFrameFlyweight.respondFlag(f)) - .next() - .then(); - - StepVerifier.create(keepAliveResponse).expectComplete().verify(Duration.ofSeconds(5)); + void clientResumptionState() { + ReplayProcessor resumePositions = ReplayProcessor.create(); + clientConnection.receiveResumePositions(new TestResumeStateHolder()).subscribe(resumePositions); + + clientConnection.sendOne(setupFrame()).subscribe(); + clientConnection.receive().subscribe(); + + testConnection.addToReceivedBuffer(keepAliveFrame(false, 1)); + testConnection.addToReceivedBuffer(keepAliveFrame(false, 2)); + testConnection.addToReceivedBuffer(keepAliveFrame(false, 3)); + + Mono.delay(Duration.ofMillis(500)).block(); + + List receivedPositions = + resumePositions.take(3).timeout(Duration.ofMillis(100)).collectList().block(); + + Collection sent = testConnection.getSent(); + List sentPositions = + sent.stream() + .filter(f -> frameType(f) != FrameType.SETUP) + .map(KeepAliveFrameFlyweight::lastPosition) + .limit(4) + .collect(Collectors.toList()); + + Assertions.assertThat(sentPositions).isEqualTo(Arrays.asList(1L, 5L, 6L, 8L)); + Assertions.assertThat(receivedPositions).isEqualTo(Arrays.asList(1L, 2L, 3L)); + + sent.forEach(ReferenceCountUtil::safeRelease); } - static class TestData { - private final RSocket rSocket; - private final Errors errors; - private final TestDuplexConnection connection; + @Test + void serverFrames() { + serverConnection.sendOne(setupFrame()).subscribe(); + serverConnection.receive().subscribe(); + testConnection.addToReceivedBuffer(keepAliveFrame(true, 0)); - public TestData(RSocket rSocket, Errors errors, TestDuplexConnection connection) { - this.rSocket = rSocket; - this.errors = errors; - this.connection = connection; - } + Assertions.assertThat(errors).isEmpty(); + Assertions.assertThat(clientConnection.isDisposed()).isFalse(); - public TestDuplexConnection connection() { - return connection; - } + Collection sent = testConnection.getSent(); + Collection sentAfterSetup = + sent.stream().filter(f -> frameType(f) != FrameType.SETUP).collect(Collectors.toList()); + Assertions.assertThat(sentAfterSetup).isNotEmpty(); - public RSocket rSocket() { - return rSocket; - } + sentAfterSetup.forEach( + f -> { + Assertions.assertThat(frameType(f)).isEqualTo(FrameType.KEEPALIVE); + Assertions.assertThat(KeepAliveFrameFlyweight.respondFlag(f)).isEqualTo(false); + Assertions.assertThat(KeepAliveFrameFlyweight.lastPosition(f)).isEqualTo(0); + }); - public Errors errors() { - return errors; - } + sent.forEach(ReferenceCountUtil::safeRelease); } - static class Errors implements Consumer { - private final List errors = new ArrayList<>(); + @Test + void serverCloseOnMissingKeepalives() { + serverConnection.sendOne(setupFrame()).subscribe(); - @Override - public void accept(Throwable throwable) { - errors.add(throwable); - } + Mono.delay(Duration.ofSeconds(1)).block(); + + Assertions.assertThat(errors).hasSize(1); + Throwable err = errors.get(0); + Assertions.assertThat(err).isExactlyInstanceOf(ConnectionErrorException.class); + Assertions.assertThat(err.getMessage()).isEqualTo("No keep-alive acks for 700 ms"); + Assertions.assertThat(clientConnection.isDisposed()).isTrue(); + + testConnection.getSent().forEach(ReferenceCountUtil::safeRelease); + } - public List errors() { - return new ArrayList<>(errors); + private ByteBuf keepAliveFrame(boolean respond, int pos) { + return KeepAliveFrameFlyweight.encode(allocator, respond, pos, Unpooled.EMPTY_BUFFER); + } + + private ByteBuf setupFrame() { + return SetupFrameFlyweight.encode( + allocator, + false, + TICK_PERIOD, + TIMEOUT, + "metadataType", + "dataType", + byteBuf("metadata"), + byteBuf("data")); + } + + private ByteBuf byteBuf(String msg) { + return Unpooled.wrappedBuffer(msg.getBytes(StandardCharsets.UTF_8)); + } + + private static FrameType frameType(ByteBuf frame) { + return FrameHeaderFlyweight.frameType(frame); + } + + private static class TestResumeStateHolder implements ResumeStateHolder { + private List positions = Arrays.asList(1, 5, 6, 8); + private int counter = 0; + + @Override + public long impliedPosition() { + Integer res = positions.get(counter); + counter = Math.min(counter + 1, positions.size() - 1); + return res; } } } diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java index dd55b44aa..2494cbbca 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java @@ -54,11 +54,6 @@ public class RSocketClientTest { @Rule public final ClientSocketRule rule = new ClientSocketRule(); - @Test(timeout = 2_000) - public void testKeepAlive() throws Exception { - assertThat("Unexpected frame sent.", frameType(rule.connection.awaitSend()), is(KEEPALIVE)); - } - @Test(timeout = 2_000) public void testInvalidFrameOnStream0() { rule.connection.addToReceivedBuffer( @@ -265,10 +260,7 @@ protected RSocketClient newRSocket() { connection, DefaultPayload::create, throwable -> errors.add(throwable), - StreamIdSupplier.clientSupplier(), - Duration.ofMillis(100), - Duration.ofMillis(10_000), - 4); + StreamIdSupplier.clientSupplier()); } public int getStreamIdForRequestType(FrameType expectedFrameType) { diff --git a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java index 57070ad69..16b7eafb5 100644 --- a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java @@ -142,7 +142,6 @@ public void connect() { SetupFrameFlyweight.encode( ByteBufAllocator.DEFAULT, false, - false, 0, 42, "mdMime", diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameFlyweightTest.java new file mode 100644 index 000000000..cf4a7063f --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameFlyweightTest.java @@ -0,0 +1,39 @@ +/* + * Copyright 2015-2019 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.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import java.util.Arrays; +import org.junit.Assert; +import org.junit.jupiter.api.Test; + +public class ResumeFrameFlyweightTest { + + @Test + void testEncoding() { + byte[] tokenBytes = new byte[65000]; + Arrays.fill(tokenBytes, (byte) 1); + ByteBuf byteBuf = ResumeFrameFlyweight.encode(ByteBufAllocator.DEFAULT, tokenBytes, 21, 12); + Assert.assertEquals( + ResumeFrameFlyweight.CURRENT_VERSION, ResumeFrameFlyweight.version(byteBuf)); + Assert.assertArrayEquals(tokenBytes, ResumeFrameFlyweight.token(byteBuf)); + Assert.assertEquals(21, ResumeFrameFlyweight.lastReceivedServerPos(byteBuf)); + Assert.assertEquals(12, ResumeFrameFlyweight.firstAvailableClientPos(byteBuf)); + byteBuf.release(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameFlyweightTest.java new file mode 100644 index 000000000..c73409ffa --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameFlyweightTest.java @@ -0,0 +1,16 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import org.junit.Assert; +import org.junit.Test; + +public class ResumeOkFrameFlyweightTest { + + @Test + public void testEncoding() { + ByteBuf byteBuf = ResumeOkFrameFlyweight.encode(ByteBufAllocator.DEFAULT, 42); + Assert.assertEquals(42, ResumeOkFrameFlyweight.lastReceivedClientPos(byteBuf)); + byteBuf.release(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java index 36c9946aa..bbdad87b5 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java @@ -1,33 +1,25 @@ package io.rsocket.frame; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.*; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; -import java.nio.charset.StandardCharsets; +import java.util.Arrays; import org.junit.jupiter.api.Test; class SetupFrameFlyweightTest { @Test - void validFrame() { + void testEncodingNoResume() { ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); ByteBuf frame = SetupFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, - false, - false, - 5, - 500, - "metadata_type", - "data_type", - metadata, - data); + ByteBufAllocator.DEFAULT, false, 5, 500, "metadata_type", "data_type", metadata, data); assertEquals(FrameType.SETUP, FrameHeaderFlyweight.frameType(frame)); + assertFalse(SetupFrameFlyweight.resumeEnabled(frame)); + assertNull(SetupFrameFlyweight.resumeToken(frame)); assertEquals("metadata_type", SetupFrameFlyweight.metadataMimeType(frame)); assertEquals("data_type", SetupFrameFlyweight.dataMimeType(frame)); assertEquals(metadata, SetupFrameFlyweight.metadata(frame)); @@ -37,39 +29,32 @@ void validFrame() { } @Test - void resumeNotSupported() { - assertThrows( - IllegalArgumentException.class, - () -> - SetupFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, - false, - true, - 5, - 500, - "", - "", - Unpooled.EMPTY_BUFFER, - Unpooled.EMPTY_BUFFER)); - } - - @Test - public void testEncoding() { + void testEncodingResume() { + byte[] tokenBytes = new byte[65000]; + Arrays.fill(tokenBytes, (byte) 1); + ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); + ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); ByteBuf frame = SetupFrameFlyweight.encode( ByteBufAllocator.DEFAULT, - false, - false, - 5000, - 60000, - "mdmt", - "dmt", - Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), - Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - frame = FrameLengthFlyweight.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); - assertEquals( - "00002100000000050000010000000013880000ea60046d646d7403646d740000026d6464", - ByteBufUtil.hexDump(frame)); + true, + 5, + 500, + Unpooled.wrappedBuffer(tokenBytes), + "metadata_type", + "data_type", + metadata, + data); + + assertEquals(FrameType.SETUP, FrameHeaderFlyweight.frameType(frame)); + assertTrue(SetupFrameFlyweight.honorLease(frame)); + assertTrue(SetupFrameFlyweight.resumeEnabled(frame)); + assertArrayEquals(tokenBytes, SetupFrameFlyweight.resumeToken(frame)); + assertEquals("metadata_type", SetupFrameFlyweight.metadataMimeType(frame)); + assertEquals("data_type", SetupFrameFlyweight.dataMimeType(frame)); + assertEquals(metadata, SetupFrameFlyweight.metadata(frame)); + assertEquals(data, SetupFrameFlyweight.data(frame)); + assertEquals(SetupFrameFlyweight.CURRENT_VERSION, SetupFrameFlyweight.version(frame)); frame.release(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java index 6cbf050ac..3b03429a6 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java @@ -54,7 +54,7 @@ public void testSplits() { .doOnNext(f -> serverFrames.incrementAndGet()) .subscribe(); multiplexer - .asStreamZeroConnection() + .asSetupConnection() .receive() .doOnNext(f -> connectionFrames.incrementAndGet()) .subscribe(); diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ResumeCacheTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeCacheTest.java deleted file mode 100644 index 18ab589dc..000000000 --- a/rsocket-core/src/test/java/io/rsocket/resume/ResumeCacheTest.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2015-2018 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.resume; - -public class ResumeCacheTest { - /*private Frame CANCEL = Frame.Cancel.from(1); - private Frame STREAM = - Frame.Request.from(1, FrameType.REQUEST_STREAM, DefaultPayload.create("Test"), 100); - - private ResumeCache cache = new ResumeCache(ResumePositionCounter.frames(), 2); - - @Test - public void startsEmpty() { - Flux x = cache.resend(0); - assertEquals(0L, (long) x.count().block()); - cache.updateRemotePosition(0); - } - - @Test(expected = IllegalStateException.class) - public void failsForFutureUpdatePosition() { - cache.updateRemotePosition(1); - } - - @Test(expected = IllegalStateException.class) - public void failsForFutureResend() { - cache.resend(1); - } - - @Test - public void updatesPositions() { - assertEquals(0, cache.getRemotePosition()); - assertEquals(0, cache.getCurrentPosition()); - assertEquals(0, cache.getEarliestResendPosition()); - assertEquals(0, cache.size()); - - cache.sent(STREAM); - - assertEquals(0, cache.getRemotePosition()); - assertEquals(14, cache.getCurrentPosition()); - assertEquals(0, cache.getEarliestResendPosition()); - assertEquals(1, cache.size()); - - cache.updateRemotePosition(14); - - assertEquals(14, cache.getRemotePosition()); - assertEquals(14, cache.getCurrentPosition()); - assertEquals(14, cache.getEarliestResendPosition()); - assertEquals(0, cache.size()); - - cache.sent(CANCEL); - - assertEquals(14, cache.getRemotePosition()); - assertEquals(20, cache.getCurrentPosition()); - assertEquals(14, cache.getEarliestResendPosition()); - assertEquals(1, cache.size()); - - cache.updateRemotePosition(20); - - assertEquals(20, cache.getRemotePosition()); - assertEquals(20, cache.getCurrentPosition()); - assertEquals(20, cache.getEarliestResendPosition()); - assertEquals(0, cache.size()); - - cache.sent(STREAM); - - assertEquals(20, cache.getRemotePosition()); - assertEquals(34, cache.getCurrentPosition()); - assertEquals(20, cache.getEarliestResendPosition()); - assertEquals(1, cache.size()); - } - - @Test - public void supportsZeroBuffer() { - cache = new ResumeCache(ResumePositionCounter.frames(), 0); - - cache.sent(STREAM); - cache.sent(STREAM); - cache.sent(STREAM); - - assertEquals(0, cache.getRemotePosition()); - assertEquals(42, cache.getCurrentPosition()); - assertEquals(42, cache.getEarliestResendPosition()); - assertEquals(0, cache.size()); - } - - @Test - public void supportsFrameCountBuffers() { - cache = new ResumeCache(ResumePositionCounter.size(), 100); - - assertEquals(0, cache.getRemotePosition()); - assertEquals(0, cache.getCurrentPosition()); - assertEquals(0, cache.getEarliestResendPosition()); - assertEquals(0, cache.size()); - - cache.sent(STREAM); - - assertEquals(0, cache.getRemotePosition()); - assertEquals(14, cache.getCurrentPosition()); - assertEquals(0, cache.getEarliestResendPosition()); - assertEquals(14, cache.size()); - - cache.updateRemotePosition(14); - - assertEquals(14, cache.getRemotePosition()); - assertEquals(14, cache.getCurrentPosition()); - assertEquals(14, cache.getEarliestResendPosition()); - assertEquals(0, cache.size()); - - cache.sent(CANCEL); - - assertEquals(14, cache.getRemotePosition()); - assertEquals(20, cache.getCurrentPosition()); - assertEquals(14, cache.getEarliestResendPosition()); - assertEquals(6, cache.size()); - }*/ -} diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java new file mode 100644 index 000000000..a9d6d235f --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2015-2019 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.resume; + +import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import reactor.test.StepVerifier; + +public class ResumeCalculatorTest { + + private ResumedFramesCalculator clientResumeCalculator; + private ResumedFramesCalculator serverResumeCalculator; + + @BeforeEach + void setUp() { + clientResumeCalculator = ResumedFramesCalculator.ofClient; + serverResumeCalculator = ResumedFramesCalculator.ofServer; + } + + @Test + void clientResumeSuccess() { + ResumptionState local = ResumptionState.fromClient(1, 42); + ResumptionState remote = ResumptionState.fromServer(3); + StepVerifier.create(clientResumeCalculator.calculate(local, remote)) + .expectNext(3L) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @Test + void clientResumeError() { + ResumptionState local = ResumptionState.fromClient(4, 42); + ResumptionState remote = ResumptionState.fromServer(3); + StepVerifier.create(clientResumeCalculator.calculate(local, remote)) + .expectError(ResumeStateException.class) + .verify(Duration.ofSeconds(1)); + } + + @Test + void serverResumeSuccess() { + ResumptionState local = ResumptionState.fromClient(1, 42); + ResumptionState remote = ResumptionState.fromClient(4, 23); + StepVerifier.create(serverResumeCalculator.calculate(local, remote)) + .expectNext(23L) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @Test + void serverResumeErrorClientState() { + ResumptionState local = ResumptionState.fromClient(1, 3); + ResumptionState remote = ResumptionState.fromClient(4, 23); + StepVerifier.create(serverResumeCalculator.calculate(local, remote)) + .expectError(ResumeStateException.class) + .verify(Duration.ofSeconds(1)); + } + + @Test + void serverResumeErrorServerState() { + ResumptionState local = ResumptionState.fromClient(4, 42); + ResumptionState remote = ResumptionState.fromClient(4, 1); + StepVerifier.create(serverResumeCalculator.calculate(local, remote)) + .expectError(ResumeStateException.class) + .verify(Duration.ofSeconds(1)); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ResumeExpBackoffTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeExpBackoffTest.java new file mode 100644 index 000000000..d86276466 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/resume/ResumeExpBackoffTest.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015-2019 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.resume; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.time.Duration; +import java.util.List; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +public class ResumeExpBackoffTest { + + @Test + void backOffSeries() { + Duration firstBackoff = Duration.ofSeconds(1); + Duration maxBackoff = Duration.ofSeconds(32); + int factor = 2; + ExponentialBackoffResumeStrategy strategy = + new ExponentialBackoffResumeStrategy(firstBackoff, maxBackoff, factor); + + List expected = + Flux.just(1, 2, 4, 8, 16, 32, 32).map(Duration::ofSeconds).collectList().block(); + + List actual = Flux.range(1, 7).map(v -> strategy.next()).collectList().block(); + + Assertions.assertThat(actual).isEqualTo(expected); + } + + @Test + void nullFirstBackoff() { + assertThrows( + NullPointerException.class, + () -> { + ExponentialBackoffResumeStrategy strategy = + new ExponentialBackoffResumeStrategy(Duration.ofSeconds(1), null, 42); + }); + } + + @Test + void nullMaxBackoff() { + assertThrows( + NullPointerException.class, + () -> { + ExponentialBackoffResumeStrategy strategy = + new ExponentialBackoffResumeStrategy(null, Duration.ofSeconds(1), 42); + }); + } + + @Test + void negativeFactor() { + assertThrows( + IllegalArgumentException.class, + () -> { + ExponentialBackoffResumeStrategy strategy = + new ExponentialBackoffResumeStrategy( + Duration.ofSeconds(1), Duration.ofSeconds(32), -1); + }); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ResumeUtilTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeUtilTest.java deleted file mode 100644 index fdd308d10..000000000 --- a/rsocket-core/src/test/java/io/rsocket/resume/ResumeUtilTest.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2015-2018 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.resume; - -public class ResumeUtilTest { - /*private Frame CANCEL = Frame.Cancel.from(1); - private Frame STREAM = - Frame.Request.from(1, FrameType.REQUEST_STREAM, DefaultPayload.create("Test"), 100); - - @Test - public void testSupportedTypes() { - assertTrue(ResumeUtil.isTracked(FrameType.REQUEST_STREAM)); - assertTrue(ResumeUtil.isTracked(FrameType.REQUEST_CHANNEL)); - assertTrue(ResumeUtil.isTracked(FrameType.REQUEST_RESPONSE)); - assertTrue(ResumeUtil.isTracked(FrameType.REQUEST_N)); - assertTrue(ResumeUtil.isTracked(FrameType.CANCEL)); - assertTrue(ResumeUtil.isTracked(FrameType.ERROR)); - assertTrue(ResumeUtil.isTracked(FrameType.REQUEST_FNF)); - assertTrue(ResumeUtil.isTracked(FrameType.PAYLOAD)); - } - - @Test - public void testUnsupportedTypes() { - assertFalse(ResumeUtil.isTracked(FrameType.METADATA_PUSH)); - assertFalse(ResumeUtil.isTracked(FrameType.RESUME)); - assertFalse(ResumeUtil.isTracked(FrameType.RESUME_OK)); - assertFalse(ResumeUtil.isTracked(FrameType.SETUP)); - assertFalse(ResumeUtil.isTracked(FrameType.EXT)); - assertFalse(ResumeUtil.isTracked(FrameType.KEEPALIVE)); - } - - @Test - public void testOffset() { - assertEquals(6, ResumeUtil.offset(CANCEL)); - assertEquals(14, ResumeUtil.offset(STREAM)); - }*/ -} diff --git a/rsocket-examples/build.gradle b/rsocket-examples/build.gradle index 04cd59c50..5f63b0761 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -26,6 +26,9 @@ dependencies { testImplementation project(':rsocket-test') testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.mockito:mockito-core' + testImplementation 'org.assertj:assertj-core' + testImplementation 'io.projectreactor:reactor-test' + testImplementation 'ch.qos.logback:logback-classic' // TODO: Remove after JUnit5 migration testCompileOnly 'junit:junit' diff --git a/rsocket-examples/src/test/java/io/rsocket/resume/DisconnectableClientTransport.java b/rsocket-examples/src/test/java/io/rsocket/resume/DisconnectableClientTransport.java new file mode 100644 index 000000000..e29066f02 --- /dev/null +++ b/rsocket-examples/src/test/java/io/rsocket/resume/DisconnectableClientTransport.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015-2019 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.resume; + +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; + +class DisconnectableClientTransport implements ClientTransport { + private final ClientTransport clientTransport; + private final AtomicReference curConnection = new AtomicReference<>(); + private long nextConnectPermitMillis; + + public DisconnectableClientTransport(ClientTransport clientTransport) { + this.clientTransport = clientTransport; + } + + @Override + public Mono connect(int mtu) { + return Mono.defer( + () -> + now() < nextConnectPermitMillis + ? Mono.error(new ClosedChannelException()) + : clientTransport + .connect(mtu) + .map( + c -> { + if (curConnection.compareAndSet(null, c)) { + return c; + } else { + throw new IllegalStateException( + "Transport supports at most 1 connection"); + } + })); + } + + public void disconnect() { + disconnectFor(Duration.ZERO); + } + + public void disconnectPermanently() { + disconnectFor(Duration.ofDays(42)); + } + + public void disconnectFor(Duration cooldown) { + DuplexConnection cur = curConnection.getAndSet(null); + if (cur != null) { + nextConnectPermitMillis = now() + cooldown.toMillis(); + cur.dispose(); + } else { + throw new IllegalStateException("Trying to disconnect while not connected"); + } + } + + private static long now() { + return System.currentTimeMillis(); + } +} diff --git a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java new file mode 100644 index 000000000..7fc3c8bee --- /dev/null +++ b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java @@ -0,0 +1,245 @@ +/* + * Copyright 2015-2019 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.resume; + +import io.rsocket.AbstractRSocket; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.RSocketFactory; +import io.rsocket.exceptions.RejectedResumeException; +import io.rsocket.exceptions.UnsupportedSetupException; +import io.rsocket.test.SlowTest; +import io.rsocket.transport.ClientTransport; +import io.rsocket.transport.ServerTransport; +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.net.InetSocketAddress; +import java.nio.channels.ClosedChannelException; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.ReplayProcessor; +import reactor.test.StepVerifier; + +@SlowTest +public class ResumeIntegrationTest { + private static final String SERVER_HOST = "localhost"; + private static final int SERVER_PORT = 0; + + @Test + void timeoutOnPermanentDisconnect() { + CloseableChannel closeable = newServerRSocket().block(); + + DisconnectableClientTransport clientTransport = + new DisconnectableClientTransport(clientTransport(closeable.address())); + + int sessionDurationSeconds = 5; + RSocket rSocket = newClientRSocket(clientTransport, sessionDurationSeconds).block(); + + Mono.delay(Duration.ofSeconds(1)).subscribe(v -> clientTransport.disconnectPermanently()); + + StepVerifier.create( + rSocket.requestChannel(testRequest()).then().doFinally(s -> closeable.dispose())) + .expectError(ClosedChannelException.class) + .verify(Duration.ofSeconds(7)); + } + + @Test + public void reconnectOnDisconnect() { + CloseableChannel closeable = newServerRSocket().block(); + + DisconnectableClientTransport clientTransport = + new DisconnectableClientTransport(clientTransport(closeable.address())); + + int sessionDurationSeconds = 15; + RSocket rSocket = newClientRSocket(clientTransport, sessionDurationSeconds).block(); + + Flux.just(3, 20, 40, 75) + .flatMap(v -> Mono.delay(Duration.ofSeconds(v))) + .subscribe(v -> clientTransport.disconnectFor(Duration.ofSeconds(7))); + + AtomicInteger counter = new AtomicInteger(-1); + StepVerifier.create( + rSocket + .requestChannel(testRequest()) + .take(Duration.ofSeconds(120)) + .map(Payload::getDataUtf8) + .timeout(Duration.ofSeconds(12)) + .doOnNext(x -> throwOnNonContinuous(counter, x)) + .then() + .doFinally(s -> closeable.dispose())) + .expectComplete() + .verify(); + } + + @Test + public void reconnectOnMissingSession() { + + int serverSessionDuration = 2; + + CloseableChannel closeable = newServerRSocket(serverSessionDuration).block(); + + DisconnectableClientTransport clientTransport = + new DisconnectableClientTransport(clientTransport(closeable.address())); + ErrorConsumer errorConsumer = new ErrorConsumer(); + int clientSessionDurationSeconds = 10; + + RSocket rSocket = + newClientRSocket(clientTransport, clientSessionDurationSeconds, errorConsumer).block(); + + Mono.delay(Duration.ofSeconds(1)) + .subscribe(v -> clientTransport.disconnectFor(Duration.ofSeconds(3))); + + StepVerifier.create( + rSocket.requestChannel(testRequest()).then().doFinally(s -> closeable.dispose())) + .expectError() + .verify(Duration.ofSeconds(5)); + + StepVerifier.create(errorConsumer.errors().next()) + .expectNextMatches( + err -> + err instanceof RejectedResumeException + && "unknown resume token".equals(err.getMessage())) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void serverMissingResume() { + CloseableChannel closeableChannel = + RSocketFactory.receive() + .acceptor((setupPayload, rSocket) -> Mono.just(new TestResponderRSocket())) + .transport(serverTransport(SERVER_HOST, SERVER_PORT)) + .start() + .block(); + + ErrorConsumer errorConsumer = new ErrorConsumer(); + + RSocket rSocket = + RSocketFactory.connect() + .resume() + .errorConsumer(errorConsumer) + .transport(clientTransport(closeableChannel.address())) + .start() + .block(); + + StepVerifier.create(errorConsumer.errors().next().doFinally(s -> closeableChannel.dispose())) + .expectNextMatches( + err -> + err instanceof UnsupportedSetupException + && "resume not supported".equals(err.getMessage())) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + StepVerifier.create(rSocket.onClose()).expectComplete().verify(Duration.ofSeconds(5)); + Assertions.assertThat(rSocket.isDisposed()).isTrue(); + } + + static ClientTransport clientTransport(InetSocketAddress address) { + return TcpClientTransport.create(address); + } + + static ServerTransport serverTransport(String host, int port) { + return TcpServerTransport.create(host, port); + } + + private static class ErrorConsumer implements Consumer { + private final ReplayProcessor errors = ReplayProcessor.create(); + + public Flux errors() { + return errors; + } + + @Override + public void accept(Throwable throwable) { + errors.onNext(throwable); + } + } + + private static Flux testRequest() { + return Flux.interval(Duration.ofMillis(50)) + .map(v -> DefaultPayload.create("client_request")) + .onBackpressureDrop(); + } + + private void throwOnNonContinuous(AtomicInteger counter, String x) { + int curValue = Integer.parseInt(x); + int prevValue = counter.get(); + if (prevValue >= 0) { + int dif = curValue - prevValue; + if (dif != 1) { + throw new IllegalStateException( + String.format( + "Payload values are expected to be continuous numbers: %d %d", + prevValue, curValue)); + } + } + counter.set(curValue); + } + + private static Mono newClientRSocket( + DisconnectableClientTransport clientTransport, int sessionDurationSeconds) { + return newClientRSocket(clientTransport, sessionDurationSeconds, err -> {}); + } + + private static Mono newClientRSocket( + DisconnectableClientTransport clientTransport, + int sessionDurationSeconds, + Consumer errConsumer) { + return RSocketFactory.connect() + .resume() + .resumeSessionDuration(Duration.ofSeconds(sessionDurationSeconds)) + .keepAliveTickPeriod(Duration.ofSeconds(1)) + .errorConsumer(errConsumer) + .resumeStrategy(() -> new PeriodicResumeStrategy(Duration.ofSeconds(1))) + .transport(clientTransport) + .start(); + } + + private static Mono newServerRSocket() { + return newServerRSocket(15); + } + + private static Mono newServerRSocket(int sessionDurationSeconds) { + return RSocketFactory.receive() + .resume() + .resumeSessionDuration(Duration.ofSeconds(sessionDurationSeconds)) + .acceptor((setupPayload, rSocket) -> Mono.just(new TestResponderRSocket())) + .transport(serverTransport(SERVER_HOST, SERVER_PORT)) + .start(); + } + + private static class TestResponderRSocket extends AbstractRSocket { + + AtomicInteger counter = new AtomicInteger(); + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.interval(Duration.ofMillis(1)) + .onBackpressureLatest() + .map(v -> DefaultPayload.create(String.valueOf(counter.getAndIncrement()))) + .takeUntilOther(Flux.from(payloads).then()); + } + } +} diff --git a/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java index 18c23057a..918c8edbf 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java @@ -99,7 +99,6 @@ public static ByteBuf createTestSetupFrame() { return SetupFrameFlyweight.encode( allocator, false, - false, 1, 1, Unpooled.EMPTY_BUFFER, diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java index 7b33bcf94..beeec93b0 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java @@ -6,6 +6,7 @@ import io.netty.channel.EventLoop; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; +import java.nio.channels.ClosedChannelException; import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; @@ -18,6 +19,7 @@ import reactor.core.Fuseable; import reactor.core.publisher.Flux; import reactor.core.publisher.Operators; +import reactor.netty.FutureMono; import reactor.util.concurrent.Queues; class SendPublisher extends Flux { @@ -141,6 +143,9 @@ private class InnerSubscriber implements Subscriber { private InnerSubscriber(CoreSubscriber destination) { this.destination = destination; + FutureMono.from(channel.closeFuture()) + .doFinally(s -> onError(new ClosedChannelException())) + .subscribe(); } @Override @@ -167,8 +172,10 @@ public void onError(Throwable t) { s.cancel(); destination.onError(t); } finally { - if (!queue.isEmpty()) { - queue.forEach(ReferenceCountUtil::safeRelease); + ByteBuf byteBuf = queue.poll(); + while (byteBuf != null) { + ReferenceCountUtil.safeRelease(byteBuf); + byteBuf = queue.poll(); } } } From f55cf8f237e20eb0237ddef42dd9804e98ba6df2 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 17 Mar 2019 23:09:10 +0200 Subject: [PATCH 014/181] General performance improvements (#598) * provides performance improvements * removes UnboundProcessor optimizations. Not possible today * fixes jmh test * polishes pollution * Update RSocketPerf.java * fixes format Signed-off-by: Oleh Dokuka --- build.gradle | 4 +- rsocket-core/jmh.gradle | 2 + .../java/io/rsocket/MaxPerfSubscriber.java | 38 ++ .../jmh/java/io/rsocket/PerfSubscriber.java | 42 ++ .../src/jmh/java/io/rsocket/RSocketPerf.java | 142 ++++++ .../main/java/io/rsocket/RSocketClient.java | 459 +++++++++--------- .../main/java/io/rsocket/RSocketServer.java | 155 ++++-- .../internal/LimitableRequestPublisher.java | 90 ++-- .../src/test/java/io/rsocket/RSocketTest.java | 10 +- .../FragmentationDuplexConnectionTest.java | 2 - .../LimitableRequestPublisherTest.java | 33 ++ .../tcp/channel/ChannelEchoClient.java | 50 +- .../transport/local/LocalClientTransport.java | 7 +- .../local/LocalDuplexConnection.java | 15 +- 14 files changed, 717 insertions(+), 332 deletions(-) create mode 100644 rsocket-core/src/jmh/java/io/rsocket/MaxPerfSubscriber.java create mode 100644 rsocket-core/src/jmh/java/io/rsocket/PerfSubscriber.java create mode 100644 rsocket-core/src/jmh/java/io/rsocket/RSocketPerf.java create mode 100644 rsocket-core/src/test/java/io/rsocket/internal/LimitableRequestPublisherTest.java diff --git a/build.gradle b/build.gradle index d8919e8ea..d9b54696e 100644 --- a/build.gradle +++ b/build.gradle @@ -20,8 +20,8 @@ plugins { id 'com.github.sherter.google-java-format' version '0.7.1' apply false id 'com.jfrog.artifactory' version '4.7.3' apply false id 'com.jfrog.bintray' version '1.8.4' apply false - id 'me.champeau.gradle.jmh' version '0.4.7' apply false - id 'io.spring.dependency-management' version '1.0.6.RELEASE' apply false + id 'me.champeau.gradle.jmh' version '0.4.8' apply false + id 'io.spring.dependency-management' version '1.0.7.RELEASE' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false } diff --git a/rsocket-core/jmh.gradle b/rsocket-core/jmh.gradle index b7cd10a13..2a2b4d7cd 100644 --- a/rsocket-core/jmh.gradle +++ b/rsocket-core/jmh.gradle @@ -19,6 +19,8 @@ dependencies { jmh configurations.implementation jmh 'org.openjdk.jmh:jmh-core' jmh 'org.openjdk.jmh:jmh-generator-annprocess' + jmh 'io.projectreactor:reactor-test' + jmh project(':rsocket-transport-local') } jmhCompileGeneratedClasses.enabled = false diff --git a/rsocket-core/src/jmh/java/io/rsocket/MaxPerfSubscriber.java b/rsocket-core/src/jmh/java/io/rsocket/MaxPerfSubscriber.java new file mode 100644 index 000000000..ace985a39 --- /dev/null +++ b/rsocket-core/src/jmh/java/io/rsocket/MaxPerfSubscriber.java @@ -0,0 +1,38 @@ +package io.rsocket; + +import java.util.concurrent.CountDownLatch; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; + +public class MaxPerfSubscriber implements CoreSubscriber { + + final CountDownLatch latch = new CountDownLatch(1); + final Blackhole blackhole; + + public MaxPerfSubscriber(Blackhole blackhole) { + this.blackhole = blackhole; + } + + @Override + public void onSubscribe(Subscription s) { + s.request(Long.MAX_VALUE); + } + + @Override + public void onNext(Payload payload) { + payload.release(); + blackhole.consume(payload); + } + + @Override + public void onError(Throwable t) { + blackhole.consume(t); + latch.countDown(); + } + + @Override + public void onComplete() { + latch.countDown(); + } +} diff --git a/rsocket-core/src/jmh/java/io/rsocket/PerfSubscriber.java b/rsocket-core/src/jmh/java/io/rsocket/PerfSubscriber.java new file mode 100644 index 000000000..98c5edd3b --- /dev/null +++ b/rsocket-core/src/jmh/java/io/rsocket/PerfSubscriber.java @@ -0,0 +1,42 @@ +package io.rsocket; + +import java.util.concurrent.CountDownLatch; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; + +public class PerfSubscriber implements CoreSubscriber { + + final CountDownLatch latch = new CountDownLatch(1); + final Blackhole blackhole; + + Subscription s; + + public PerfSubscriber(Blackhole blackhole) { + this.blackhole = blackhole; + } + + @Override + public void onSubscribe(Subscription s) { + this.s = s; + s.request(1); + } + + @Override + public void onNext(Payload payload) { + payload.release(); + blackhole.consume(payload); + s.request(1); + } + + @Override + public void onError(Throwable t) { + blackhole.consume(t); + latch.countDown(); + } + + @Override + public void onComplete() { + latch.countDown(); + } +} diff --git a/rsocket-core/src/jmh/java/io/rsocket/RSocketPerf.java b/rsocket-core/src/jmh/java/io/rsocket/RSocketPerf.java new file mode 100644 index 000000000..476d6c814 --- /dev/null +++ b/rsocket-core/src/jmh/java/io/rsocket/RSocketPerf.java @@ -0,0 +1,142 @@ +package io.rsocket; + +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.transport.local.LocalClientTransport; +import io.rsocket.transport.local.LocalServerTransport; +import io.rsocket.util.EmptyPayload; +import java.util.stream.IntStream; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import org.reactivestreams.Publisher; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@BenchmarkMode(Mode.Throughput) +@Fork( + value = 1 // , jvmArgsAppend = {"-Dio.netty.leakDetection.level=advanced"} + ) +@Warmup(iterations = 10) +@Measurement(iterations = 10, time = 20) +@State(Scope.Benchmark) +public class RSocketPerf { + + static final Payload PAYLOAD = EmptyPayload.INSTANCE; + static final Mono PAYLOAD_MONO = Mono.just(PAYLOAD); + static final Flux PAYLOAD_FLUX = + Flux.fromArray(IntStream.range(0, 100000).mapToObj(__ -> PAYLOAD).toArray(Payload[]::new)); + + RSocket client; + Closeable server; + + @Setup + public void setUp() { + server = + RSocketFactory.receive() + .acceptor( + (setup, sendingSocket) -> + Mono.just( + new AbstractRSocket() { + + @Override + public Mono fireAndForget(Payload payload) { + payload.release(); + return Mono.empty(); + } + + @Override + public Mono requestResponse(Payload payload) { + payload.release(); + return PAYLOAD_MONO; + } + + @Override + public Flux requestStream(Payload payload) { + payload.release(); + return PAYLOAD_FLUX; + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads); + } + })) + .transport(LocalServerTransport.create("server")) + .start() + .block(); + + client = + RSocketFactory.connect() + .frameDecoder(PayloadDecoder.ZERO_COPY) + .transport(LocalClientTransport.create("server")) + .start() + .block(); + } + + @Benchmark + @SuppressWarnings("unchecked") + public PerfSubscriber fireAndForget(Blackhole blackhole) throws InterruptedException { + PerfSubscriber subscriber = new PerfSubscriber(blackhole); + client.fireAndForget(PAYLOAD).subscribe((CoreSubscriber) subscriber); + subscriber.latch.await(); + + return subscriber; + } + + @Benchmark + public PerfSubscriber requestResponse(Blackhole blackhole) throws InterruptedException { + PerfSubscriber subscriber = new PerfSubscriber(blackhole); + client.requestResponse(PAYLOAD).subscribe(subscriber); + subscriber.latch.await(); + + return subscriber; + } + + @Benchmark + public PerfSubscriber requestStreamWithRequestByOneStrategy(Blackhole blackhole) + throws InterruptedException { + PerfSubscriber subscriber = new PerfSubscriber(blackhole); + client.requestStream(PAYLOAD).subscribe(subscriber); + subscriber.latch.await(); + + return subscriber; + } + + @Benchmark + public MaxPerfSubscriber requestStreamWithRequestAllStrategy(Blackhole blackhole) + throws InterruptedException { + MaxPerfSubscriber subscriber = new MaxPerfSubscriber(blackhole); + client.requestStream(PAYLOAD).subscribe(subscriber); + subscriber.latch.await(); + + return subscriber; + } + + @Benchmark + public PerfSubscriber requestChannelWithRequestByOneStrategy(Blackhole blackhole) + throws InterruptedException { + PerfSubscriber subscriber = new PerfSubscriber(blackhole); + client.requestChannel(PAYLOAD_FLUX).subscribe(subscriber); + subscriber.latch.await(); + + return subscriber; + } + + @Benchmark + public MaxPerfSubscriber requestChannelWithRequestAllStrategy(Blackhole blackhole) + throws InterruptedException { + MaxPerfSubscriber subscriber = new MaxPerfSubscriber(blackhole); + client.requestChannel(PAYLOAD_FLUX).subscribe(subscriber); + subscriber.latch.await(); + + return subscriber; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java index debfc61a3..9218135de 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java @@ -29,12 +29,14 @@ import java.nio.channels.ClosedChannelException; import java.util.Collections; import java.util.Map; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; +import java.util.function.LongConsumer; +import java.util.function.Supplier; import org.reactivestreams.Processor; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; +import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.SignalType; @@ -73,14 +75,14 @@ class RSocketClient implements RSocket { connection.onClose().doFinally(signalType -> terminate()).subscribe(null, errorConsumer); - connection - .send( - sendProcessor.doOnRequest( - r -> { - for (LimitableRequestPublisher lrp : senders.values()) { - lrp.increaseInternalLimit(r); - } - })) + sendProcessor + .doOnRequest( + r -> { + for (LimitableRequestPublisher lrp : senders.values()) { + lrp.increaseInternalLimit(r); + } + }) + .transform(connection::send) .doFinally(this::handleSendProcessorCancel) .subscribe(null, this::handleSendProcessorError); @@ -169,223 +171,224 @@ public Mono onClose() { } private Mono handleFireAndForget(Payload payload) { - return lifecycle - .active() - .then( - Mono.fromRunnable( - () -> { - final int streamId = streamIdSupplier.nextStreamId(); - ByteBuf requestFrame = - RequestFireAndForgetFrameFlyweight.encode( - allocator, - streamId, - false, - payload.hasMetadata() ? payload.sliceMetadata().retain() : null, - payload.sliceData().retain()); - payload.release(); - sendProcessor.onNext(requestFrame); - })); + return lifecycle.active( + () -> { + final int streamId = streamIdSupplier.nextStreamId(); + ByteBuf requestFrame = + RequestFireAndForgetFrameFlyweight.encode( + allocator, + streamId, + false, + payload.hasMetadata() ? payload.sliceMetadata().retain() : null, + payload.sliceData().retain()); + payload.release(); + sendProcessor.onNext(requestFrame); + }); } - private Flux handleRequestStream(final Payload payload) { - return lifecycle - .active() - .thenMany( - Flux.defer( - () -> { - int streamId = streamIdSupplier.nextStreamId(); - - UnicastProcessor receiver = UnicastProcessor.create(); - receivers.put(streamId, receiver); - - AtomicBoolean first = new AtomicBoolean(false); - - return receiver - .doOnRequest( - n -> { - if (first.compareAndSet(false, true) && !receiver.isDisposed()) { - sendProcessor.onNext( - RequestStreamFrameFlyweight.encode( - allocator, - streamId, - false, - n, - payload.sliceMetadata().retain(), - payload.sliceData().retain())); - } else if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext( - RequestNFrameFlyweight.encode(allocator, streamId, n)); - } - sendProcessor.drain(); - }) - .doOnError( - t -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext( - ErrorFrameFlyweight.encode(allocator, streamId, t)); - } - }) - .doOnCancel( - () -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext( - CancelFrameFlyweight.encode(allocator, streamId)); - } - }) - .doFinally( - s -> { - receivers.remove(streamId); - }); - })); + private Mono handleRequestResponse(final Payload payload) { + return lifecycle.activeMono( + () -> { + int streamId = streamIdSupplier.nextStreamId(); + final UnboundedProcessor sendProcessor = this.sendProcessor; + final ByteBuf requestFrame = + RequestResponseFrameFlyweight.encode( + allocator, + streamId, + false, + payload.sliceMetadata().retain(), + payload.sliceData().retain()); + + payload.release(); + + UnicastMonoProcessor receiver = UnicastMonoProcessor.create(); + receivers.put(streamId, receiver); + + sendProcessor.onNext(requestFrame); + return receiver + .doOnError( + t -> sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t))) + .doFinally( + s -> { + if (s == SignalType.CANCEL) { + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + } + + receivers.remove(streamId); + }); + }); } - private Mono handleRequestResponse(final Payload payload) { - return lifecycle - .active() - .then( - Mono.defer( - () -> { - int streamId = streamIdSupplier.nextStreamId(); - ByteBuf requestFrame = - RequestResponseFrameFlyweight.encode( - allocator, - streamId, - false, - payload.sliceMetadata().retain(), - payload.sliceData().retain()); - payload.release(); - - UnicastMonoProcessor receiver = UnicastMonoProcessor.create(); - receivers.put(streamId, receiver); - - sendProcessor.onNext(requestFrame); - return receiver - .doOnError( - t -> - sendProcessor.onNext( - ErrorFrameFlyweight.encode(allocator, streamId, t))) - .doFinally( - s -> { - if (s == SignalType.CANCEL) { - sendProcessor.onNext( - CancelFrameFlyweight.encode(allocator, streamId)); - } - - receivers.remove(streamId); - }); - })); + private Flux handleRequestStream(final Payload payload) { + return lifecycle.activeFlux( + () -> { + int streamId = streamIdSupplier.nextStreamId(); + + final UnboundedProcessor sendProcessor = this.sendProcessor; + final UnicastProcessor receiver = UnicastProcessor.create(); + + receivers.put(streamId, receiver); + + return receiver + .doOnRequest( + new LongConsumer() { + + // No need to make it atomic; See + // https://github.com/reactive-streams/reactive-streams-jvm#2.7 + boolean firstRequest = true; + + @Override + public void accept(long n) { + if (firstRequest && !receiver.isDisposed()) { + firstRequest = false; + sendProcessor.onNext( + RequestStreamFrameFlyweight.encode( + allocator, + streamId, + false, + n, + payload.sliceMetadata().retain(), + payload.sliceData().retain())); + payload.release(); + } else if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); + } + } + }) + .doOnError( + t -> { + if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t)); + } + }) + .doOnCancel( + () -> { + if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + } + }) + .doFinally(s -> receivers.remove(streamId)); + }); } private Flux handleChannel(Flux request) { - return lifecycle - .active() - .thenMany( - Flux.defer( - () -> { - final UnicastProcessor receiver = UnicastProcessor.create(); - final int streamId = streamIdSupplier.nextStreamId(); - final AtomicBoolean firstRequest = new AtomicBoolean(true); - - return receiver - .doOnRequest( - n -> { - if (firstRequest.compareAndSet(true, false)) { - final AtomicBoolean firstPayload = new AtomicBoolean(true); - final Flux requestFrames = - request - .transform( - f -> { - LimitableRequestPublisher wrapped = - LimitableRequestPublisher.wrap( - f, sendProcessor.available()); - // Need to set this to one for first the frame - wrapped.increaseRequestLimit(1); - senders.put(streamId, wrapped); - receivers.put(streamId, receiver); - - return wrapped; - }) - .map( - payload -> { - final ByteBuf requestFrame; - if (firstPayload.compareAndSet(true, false)) { - requestFrame = - RequestChannelFrameFlyweight.encode( - allocator, - streamId, - false, - false, - n, - payload.sliceMetadata().retain(), - payload.sliceData().retain()); - } else { - requestFrame = - PayloadFrameFlyweight.encode( - allocator, streamId, false, false, true, - payload); - } - return requestFrame; - }) - .doOnComplete( - () -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext( - PayloadFrameFlyweight.encodeComplete( - allocator, streamId)); - } - if (firstPayload.get()) { - receiver.onComplete(); - } - }); - - requestFrames.subscribe( - sendProcessor::onNext, - t -> { + return lifecycle.activeFlux( + () -> { + final UnboundedProcessor sendProcessor = this.sendProcessor; + final UnicastProcessor receiver = UnicastProcessor.create(); + final int streamId = streamIdSupplier.nextStreamId(); + + return receiver + .doOnRequest( + new LongConsumer() { + + // No need to make it atomic; See + // https://github.com/reactive-streams/reactive-streams-jvm#2.7 + boolean firstRequest = true; + + @Override + public void accept(long n) { + if (firstRequest) { + firstRequest = false; + request + .transform( + f -> { + LimitableRequestPublisher wrapped = + LimitableRequestPublisher.wrap(f, sendProcessor.available()); + // Need to set this to one for first the frame + wrapped.request(1); + senders.put(streamId, wrapped); + receivers.put(streamId, receiver); + + return wrapped; + }) + .subscribe( + new BaseSubscriber() { + + // no need to make it atomic; See + // https://github.com/reactive-streams/reactive-streams-jvm#1.3 + boolean firstPayload = true; + + @Override + protected void hookOnNext(Payload payload) { + final ByteBuf frame; + + if (firstPayload) { + firstPayload = false; + frame = + RequestChannelFrameFlyweight.encode( + allocator, + streamId, + false, + false, + n, + payload.sliceMetadata().retain(), + payload.sliceData().retain()); + } else { + frame = + PayloadFrameFlyweight.encode( + allocator, streamId, false, false, true, payload); + } + + sendProcessor.onNext(frame); + payload.release(); + } + + @Override + protected void hookOnComplete() { + if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext( + PayloadFrameFlyweight.encodeComplete( + allocator, streamId)); + } + if (firstPayload) { + receiver.onComplete(); + } + } + + @Override + protected void hookOnError(Throwable t) { errorConsumer.accept(t); receiver.dispose(); - }); - } else { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext( - RequestNFrameFlyweight.encode(allocator, streamId, n)); - } - } - }) - .doOnError( - t -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext( - ErrorFrameFlyweight.encode(allocator, streamId, t)); - } - }) - .doOnCancel( - () -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext( - CancelFrameFlyweight.encode(allocator, streamId)); - } - }) - .doFinally( - s -> { - receivers.remove(streamId); - LimitableRequestPublisher sender = senders.remove(streamId); - if (sender != null) { - sender.cancel(); - } - }); - })); + } + }); + } else { + if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext( + RequestNFrameFlyweight.encode(allocator, streamId, n)); + } + } + } + }) + .doOnError( + t -> { + if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t)); + } + }) + .doOnCancel( + () -> { + if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + } + }) + .doFinally( + s -> { + receivers.remove(streamId); + LimitableRequestPublisher sender = senders.remove(streamId); + if (sender != null) { + sender.cancel(); + } + }); + }); } private Mono handleMetadataPush(Payload payload) { - return lifecycle - .active() - .then( - Mono.fromRunnable( - () -> { - sendProcessor.onNext( - MetadataPushFrameFlyweight.encode( - allocator, payload.sliceMetadata().retain())); - })); + return lifecycle.active( + () -> { + sendProcessor.onNext( + MetadataPushFrameFlyweight.encode(allocator, payload.sliceMetadata().retain())); + }); } private boolean contains(int streamId) { @@ -490,8 +493,7 @@ private void handleFrame(int streamId, FrameType type, ByteBuf frame) { LimitableRequestPublisher sender = senders.get(streamId); if (sender != null) { int n = RequestNFrameFlyweight.requestN(frame); - sender.increaseRequestLimit(n); - sendProcessor.drain(); + sender.request(n >= Integer.MAX_VALUE ? Long.MAX_VALUE : n); } break; } @@ -537,10 +539,11 @@ private static class Lifecycle { Lifecycle.class, Throwable.class, "terminationError"); private volatile Throwable terminationError; - public Mono active() { + public Mono active(Runnable runnable) { return Mono.create( sink -> { if (terminationError == null) { + runnable.run(); sink.success(); } else { sink.error(terminationError); @@ -548,6 +551,28 @@ public Mono active() { }); } + public Mono activeMono(Supplier> supplier) { + return Mono.defer( + () -> { + if (terminationError == null) { + return supplier.get(); + } else { + return Mono.error(terminationError); + } + }); + } + + public Flux activeFlux(Supplier> supplier) { + return Flux.defer( + () -> { + if (terminationError == null) { + return supplier.get(); + } else { + return Flux.error(terminationError); + } + }); + } + public Throwable getTerminationError() { return terminationError; } diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java index a49feafa3..5aabb0ffa 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java @@ -34,6 +34,7 @@ import org.reactivestreams.Subscription; import reactor.core.Disposable; import reactor.core.Exceptions; +import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.SignalType; @@ -79,14 +80,14 @@ class RSocketServer implements ResponderRSocket { // connections this.sendProcessor = new UnboundedProcessor<>(); - connection - .send( - sendProcessor.doOnRequest( - r -> { - for (LimitableRequestPublisher lrp : sendingLimitableSubscriptions.values()) { - lrp.increaseInternalLimit(r); - } - })) + sendProcessor + .doOnRequest( + r -> { + for (LimitableRequestPublisher lrp : sendingLimitableSubscriptions.values()) { + lrp.increaseInternalLimit(r); + } + }) + .transform(connection::send) .doFinally(this::handleSendProcessorCancel) .subscribe(null, this::handleSendProcessorError); @@ -352,37 +353,73 @@ private void handleFrame(ByteBuf frame) { } private void handleFireAndForget(int streamId, Mono result) { - result - .doOnSubscribe(subscription -> sendingSubscriptions.put(streamId, subscription)) - .doFinally(signalType -> sendingSubscriptions.remove(streamId)) - .subscribe(null, errorConsumer); + result.subscribe( + new BaseSubscriber() { + @Override + protected void hookOnSubscribe(Subscription subscription) { + sendingSubscriptions.put(streamId, subscription); + subscription.request(Long.MAX_VALUE); + } + + @Override + protected void hookOnError(Throwable throwable) { + errorConsumer.accept(throwable); + } + + @Override + protected void hookFinally(SignalType type) { + sendingSubscriptions.remove(streamId); + } + }); } private void handleRequestResponse(int streamId, Mono response) { - response - .doOnSubscribe(subscription -> sendingSubscriptions.put(streamId, subscription)) - .map( - payload -> { - ByteBuf byteBuf = null; - try { - byteBuf = PayloadFrameFlyweight.encodeNextComplete(allocator, streamId, payload); - } catch (Throwable t) { - if (byteBuf != null) { - ReferenceCountUtil.safeRelease(byteBuf); - ReferenceCountUtil.safeRelease(payload); - } - } + response.subscribe( + new BaseSubscriber() { + private boolean isEmpty = true; + + @Override + protected void hookOnSubscribe(Subscription subscription) { + sendingSubscriptions.put(streamId, subscription); + subscription.request(Long.MAX_VALUE); + } + + @Override + protected void hookOnNext(Payload payload) { + if (isEmpty) { + isEmpty = false; + } + + ByteBuf byteBuf; + try { + byteBuf = PayloadFrameFlyweight.encodeNextComplete(allocator, streamId, payload); + } catch (Throwable t) { payload.release(); - return byteBuf; - }) - .switchIfEmpty( - Mono.fromCallable(() -> PayloadFrameFlyweight.encodeComplete(allocator, streamId))) - .doFinally(signalType -> sendingSubscriptions.remove(streamId)) - .subscribe( - t1 -> { - sendProcessor.onNext(t1); - }, - t -> handleError(streamId, t)); + throw Exceptions.propagate(t); + } + + payload.release(); + + sendProcessor.onNext(byteBuf); + } + + @Override + protected void hookOnError(Throwable throwable) { + handleError(streamId, throwable); + } + + @Override + protected void hookOnComplete() { + if (isEmpty) { + sendProcessor.onNext(PayloadFrameFlyweight.encodeComplete(allocator, streamId)); + } + } + + @Override + protected void hookFinally(SignalType type) { + sendingSubscriptions.remove(streamId); + } + }); } private void handleStream(int streamId, Flux response, int initialRequestN) { @@ -392,27 +429,43 @@ private void handleStream(int streamId, Flux response, int initialReque LimitableRequestPublisher payloads = LimitableRequestPublisher.wrap(frameFlux, sendProcessor.available()); sendingLimitableSubscriptions.put(streamId, payloads); - payloads.increaseRequestLimit(initialRequestN); + payloads.request( + initialRequestN >= Integer.MAX_VALUE ? Long.MAX_VALUE : initialRequestN); return payloads; }) - .doFinally(signalType -> sendingLimitableSubscriptions.remove(streamId)) .subscribe( - payload -> { - ByteBuf byteBuf = null; - try { - byteBuf = PayloadFrameFlyweight.encodeNext(allocator, streamId, payload); - } catch (Throwable t) { - if (byteBuf != null) { - ReferenceCountUtil.safeRelease(byteBuf); - ReferenceCountUtil.safeRelease(payload); + new BaseSubscriber() { + + @Override + protected void hookOnNext(Payload payload) { + ByteBuf byteBuf; + try { + byteBuf = PayloadFrameFlyweight.encodeNext(allocator, streamId, payload); + } catch (Throwable t) { + payload.release(); + throw Exceptions.propagate(t); } - throw Exceptions.propagate(t); + + payload.release(); + + sendProcessor.onNext(byteBuf); } - payload.release(); - sendProcessor.onNext(byteBuf); - }, - t -> handleError(streamId, t), - () -> sendProcessor.onNext(PayloadFrameFlyweight.encodeComplete(allocator, streamId))); + + @Override + protected void hookOnComplete() { + sendProcessor.onNext(PayloadFrameFlyweight.encodeComplete(allocator, streamId)); + } + + @Override + protected void hookOnError(Throwable throwable) { + handleError(streamId, throwable); + } + + @Override + protected void hookFinally(SignalType type) { + sendingLimitableSubscriptions.remove(streamId); + } + }); } private void handleChannel(int streamId, Payload payload, int initialRequestN) { diff --git a/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java b/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java index d5a05375d..2eafd3d61 100755 --- a/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java @@ -16,7 +16,7 @@ package io.rsocket.internal; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; import javax.annotation.Nullable; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -27,9 +27,15 @@ /** */ public class LimitableRequestPublisher extends Flux implements Subscription { + + private static final int NOT_CANCELED_STATE = 0; + private static final int CANCELED_STATE = 1; + private final Publisher source; - private final AtomicBoolean canceled; + private volatile int canceled; + private static final AtomicIntegerFieldUpdater CANCELED = + AtomicIntegerFieldUpdater.newUpdater(LimitableRequestPublisher.class, "canceled"); private final long prefetch; @@ -37,14 +43,13 @@ public class LimitableRequestPublisher extends Flux implements Subscriptio private long externalRequested; - private volatile boolean subscribed; + private boolean subscribed; - private volatile @Nullable Subscription internalSubscription; + private @Nullable Subscription internalSubscription; private LimitableRequestPublisher(Publisher source, long prefetch) { this.source = source; this.prefetch = prefetch; - this.canceled = new AtomicBoolean(); } public static LimitableRequestPublisher wrap(Publisher source, long prefetch) { @@ -60,46 +65,57 @@ public void subscribe(CoreSubscriber destination) { subscribed = true; } + final InnerOperator s = new InnerOperator(destination); - destination.onSubscribe(new InnerSubscription()); - source.subscribe(new InnerSubscriber(destination)); + destination.onSubscribe(s); + source.subscribe(s); increaseInternalLimit(prefetch); } - public void increaseRequestLimit(long n) { + public void increaseInternalLimit(long n) { synchronized (this) { - externalRequested = Operators.addCap(n, externalRequested); + long requested = internalRequested; + if (requested == Long.MAX_VALUE) { + return; + } + internalRequested = Operators.addCap(n, requested); } requestN(); } - public void increaseInternalLimit(long n) { + @Override + public void request(long n) { synchronized (this) { - internalRequested = Operators.addCap(n, internalRequested); + long requested = externalRequested; + if (requested == Long.MAX_VALUE) { + return; + } + externalRequested = Operators.addCap(n, requested); } requestN(); } - @Override - public void request(long n) { - increaseRequestLimit(n); - } - private void requestN() { long r; + final Subscription s; + synchronized (this) { - if (internalSubscription == null) { + s = internalSubscription; + if (s == null) { return; } - if (externalRequested != Long.MAX_VALUE || internalRequested != Long.MAX_VALUE) { - r = Math.min(internalRequested, externalRequested); - if (externalRequested != Long.MAX_VALUE) { + long er = externalRequested; + long ir = internalRequested; + + if (er != Long.MAX_VALUE || ir != Long.MAX_VALUE) { + r = Math.min(ir, er); + if (er != Long.MAX_VALUE) { externalRequested -= r; } - if (internalRequested != Long.MAX_VALUE) { + if (ir != Long.MAX_VALUE) { internalRequested -= r; } } else { @@ -108,22 +124,34 @@ private void requestN() { } if (r > 0) { - internalSubscription.request(r); + s.request(r); } } public void cancel() { - if (canceled.compareAndSet(false, true) && internalSubscription != null) { - internalSubscription.cancel(); - internalSubscription = null; - subscribed = false; + if (!isCanceled() && CANCELED.compareAndSet(this, NOT_CANCELED_STATE, CANCELED_STATE)) { + Subscription s; + + synchronized (this) { + s = internalSubscription; + internalSubscription = null; + subscribed = false; + } + + if (s != null) { + s.cancel(); + } } } - private class InnerSubscriber implements Subscriber { - Subscriber destination; + private boolean isCanceled() { + return canceled == 1; + } - private InnerSubscriber(Subscriber destination) { + private class InnerOperator implements CoreSubscriber, Subscription { + final Subscriber destination; + + private InnerOperator(Subscriber destination) { this.destination = destination; } @@ -132,7 +160,7 @@ public void onSubscribe(Subscription s) { synchronized (LimitableRequestPublisher.this) { LimitableRequestPublisher.this.internalSubscription = s; - if (canceled.get()) { + if (isCanceled()) { s.cancel(); subscribed = false; LimitableRequestPublisher.this.internalSubscription = null; @@ -160,9 +188,7 @@ public void onError(Throwable t) { public void onComplete() { destination.onComplete(); } - } - private class InnerSubscription implements Subscription { @Override public void request(long n) {} diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketTest.java index 7fcf46674..9ef37a398 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketTest.java @@ -84,6 +84,12 @@ public Mono requestResponse(Payload payload) { rule.assertServerError("java.lang.NullPointerException: Deliberate exception."); } + @Test(timeout = 2000) + public void testStream() throws Exception { + Flux responses = rule.crs.requestStream(DefaultPayload.create("Payload In")); + StepVerifier.create(responses).expectNextCount(10).expectComplete().verify(); + } + @Test(timeout = 2000) public void testChannel() throws Exception { Flux requests = @@ -136,7 +142,9 @@ public Mono requestResponse(Payload payload) { @Override public Flux requestStream(Payload payload) { - return Flux.never(); + return Flux.range(1, 10) + .map( + i -> DefaultPayload.create("server got -> [" + payload.toString() + "]")); } @Override diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java index ac4413caa..19c0f1f62 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java @@ -24,7 +24,6 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; -import io.netty.util.ReferenceCountUtil; import io.rsocket.DuplexConnection; import io.rsocket.frame.*; import io.rsocket.util.DefaultPayload; @@ -125,7 +124,6 @@ void reassembleData() { .assertNext( byteBuf -> { Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); - ReferenceCountUtil.safeRelease(byteBuf); }) .verifyComplete(); } diff --git a/rsocket-core/src/test/java/io/rsocket/internal/LimitableRequestPublisherTest.java b/rsocket-core/src/test/java/io/rsocket/internal/LimitableRequestPublisherTest.java new file mode 100644 index 000000000..8c51c123e --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/internal/LimitableRequestPublisherTest.java @@ -0,0 +1,33 @@ +package io.rsocket.internal; + +import java.util.ArrayDeque; +import java.util.Queue; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.DirectProcessor; +import reactor.test.util.RaceTestUtils; + +class LimitableRequestPublisherTest { + + @Test + @RepeatedTest(2) + public void requestLimitRacingTest() throws InterruptedException { + Queue requests = new ArrayDeque<>(10000); + LimitableRequestPublisher limitableRequestPublisher = + LimitableRequestPublisher.wrap(DirectProcessor.create().doOnRequest(requests::add), 0); + + Runnable request1 = () -> limitableRequestPublisher.request(1); + Runnable request2 = () -> limitableRequestPublisher.increaseInternalLimit(2); + + limitableRequestPublisher.subscribe(); + + for (int i = 0; i < 10000; i++) { + RaceTestUtils.race(request1, request2); + } + + Thread.sleep(1000); + + Assertions.assertThat(requests.stream().mapToLong(l -> l).sum()).isEqualTo(10000); + } +} 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 386154f20..ac889ecfc 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,38 +22,42 @@ import io.rsocket.RSocket; import io.rsocket.RSocketFactory; import io.rsocket.SocketAcceptor; -import io.rsocket.transport.netty.client.TcpClientTransport; -import io.rsocket.transport.netty.server.TcpServerTransport; -import io.rsocket.util.DefaultPayload; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.transport.local.LocalClientTransport; +import io.rsocket.transport.local.LocalServerTransport; +import io.rsocket.util.ByteBufPayload; import java.time.Duration; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; public final class ChannelEchoClient { + static final Payload payload1 = ByteBufPayload.create("Hello "); public static void main(String[] args) { RSocketFactory.receive() + .frameDecoder(PayloadDecoder.ZERO_COPY) .acceptor(new SocketAcceptorImpl()) - .transport(TcpServerTransport.create("localhost", 7000)) + .transport(LocalServerTransport.create("localhost")) .start() .subscribe(); RSocket socket = RSocketFactory.connect() - .transport(TcpClientTransport.create("localhost", 7000)) + .keepAliveAckTimeout(Duration.ofMinutes(10)) + .frameDecoder(PayloadDecoder.ZERO_COPY) + .transport(LocalClientTransport.create("localhost")) .start() .block(); - socket - .requestChannel( - Flux.interval(Duration.ofMillis(1000)).map(i -> DefaultPayload.create("Hello"))) - .map(Payload::getDataUtf8) - .doOnNext(System.out::println) - .take(10) - .doFinally(signalType -> socket.dispose()) - .then() - .block(); + Flux.range(0, 100000000) + .concatMap(i -> socket.fireAndForget(payload1.retain())) + // .doOnNext(p -> { + //// System.out.println(p.getDataUtf8()); + // p.release(); + // }) + .blockLast(); } private static class SocketAcceptorImpl implements SocketAcceptor { @@ -61,12 +65,22 @@ private static class SocketAcceptorImpl implements SocketAcceptor { public Mono accept(ConnectionSetupPayload setupPayload, RSocket reactiveSocket) { return Mono.just( new AbstractRSocket() { + + @Override + public Mono fireAndForget(Payload payload) { + // System.out.println(payload.getDataUtf8()); + payload.release(); + return Mono.empty(); + } + + @Override + public Mono requestResponse(Payload payload) { + return Mono.just(payload); + } + @Override public Flux requestChannel(Publisher payloads) { - return Flux.from(payloads) - .map(Payload::getDataUtf8) - .map(s -> "Echo: " + s) - .map(DefaultPayload::create); + return Flux.from(payloads).subscribeOn(Schedulers.single()); } }); } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java index 55d6aaf93..d49ae43b4 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java @@ -27,6 +27,7 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.UnicastProcessor; +import reactor.util.concurrent.Queues; /** * An implementation of {@link ClientTransport} that connects to a {@link ServerTransport} in the @@ -61,8 +62,10 @@ private Mono connect() { return Mono.error(new IllegalArgumentException("Could not find server: " + name)); } - UnicastProcessor in = UnicastProcessor.create(); - UnicastProcessor out = UnicastProcessor.create(); + UnicastProcessor in = + UnicastProcessor.create(Queues.unboundedMultiproducer().get()); + UnicastProcessor out = + UnicastProcessor.create(Queues.unboundedMultiproducer().get()); MonoProcessor closeNotifier = MonoProcessor.create(); server.accept(new LocalDuplexConnection(out, in, closeNotifier)); diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java index a295d2b00..f9501717c 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java @@ -73,12 +73,13 @@ public Flux receive() { public Mono send(Publisher frames) { Objects.requireNonNull(frames, "frames must not be null"); - return Flux.from(frames) - .doOnNext( - byteBuf -> { - byteBuf.retain(); - out.onNext(byteBuf); - }) - .then(); + return Flux.from(frames).doOnNext(out::onNext).then(); + } + + @Override + public Mono sendOne(ByteBuf frame) { + Objects.requireNonNull(frame, "frame must not be null"); + out.onNext(frame); + return Mono.empty(); } } From 16bc03ff39dd16dadfae705726ab3e192c42c208 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Wed, 27 Mar 2019 12:11:54 +0200 Subject: [PATCH 015/181] resumption: update long running test Signed-off-by: Maksym Ostroverkhov --- .../rsocket/resume/ResumeIntegrationTest.java | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java index 7fc3c8bee..dc00d7a77 100644 --- a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java @@ -40,6 +40,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.ReplayProcessor; +import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; @SlowTest @@ -83,7 +84,7 @@ public void reconnectOnDisconnect() { StepVerifier.create( rSocket .requestChannel(testRequest()) - .take(Duration.ofSeconds(120)) + .take(Duration.ofSeconds(600)) .map(Payload::getDataUtf8) .timeout(Duration.ofSeconds(12)) .doOnNext(x -> throwOnNonContinuous(counter, x)) @@ -210,7 +211,8 @@ private static Mono newClientRSocket( return RSocketFactory.connect() .resume() .resumeSessionDuration(Duration.ofSeconds(sessionDurationSeconds)) - .keepAliveTickPeriod(Duration.ofSeconds(1)) + .keepAliveTickPeriod(Duration.ofSeconds(30)) + .keepAliveAckTimeout(Duration.ofMinutes(5)) .errorConsumer(errConsumer) .resumeStrategy(() -> new PeriodicResumeStrategy(Duration.ofSeconds(1))) .transport(clientTransport) @@ -224,6 +226,7 @@ private static Mono newServerRSocket() { private static Mono newServerRSocket(int sessionDurationSeconds) { return RSocketFactory.receive() .resume() + .resumeStore(t -> new InMemoryResumableFramesStore("server",100_000)) .resumeSessionDuration(Duration.ofSeconds(sessionDurationSeconds)) .acceptor((setupPayload, rSocket) -> Mono.just(new TestResponderRSocket())) .transport(serverTransport(SERVER_HOST, SERVER_PORT)) @@ -236,10 +239,18 @@ private static class TestResponderRSocket extends AbstractRSocket { @Override public Flux requestChannel(Publisher payloads) { - return Flux.interval(Duration.ofMillis(1)) - .onBackpressureLatest() + return duplicate(Flux.interval(Duration.ofMillis(1)) + .onBackpressureLatest().publishOn(Schedulers.elastic()), 20) .map(v -> DefaultPayload.create(String.valueOf(counter.getAndIncrement()))) .takeUntilOther(Flux.from(payloads).then()); } + + private Flux duplicate(Flux f, int n) { + Flux r =Flux.empty(); + for (int i = 0; i < n; i++) { + r = r.mergeWith(f); + } + return r; + } } } From 19e2f763333fda33d33cf7d129020554fd23b5e0 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Wed, 27 Mar 2019 14:40:00 +0200 Subject: [PATCH 016/181] update tcp ping-pong test with resumable transport case Signed-off-by: Maksym Ostroverkhov --- .../rsocket/resume/ResumeIntegrationTest.java | 11 +-- .../main/java/io/rsocket/test/PerfTest.java | 17 +++++ .../main/java/io/rsocket/test/PingClient.java | 18 ++++- .../java/io/rsocket/test/PingHandler.java | 7 ++ .../transport/local/LocalPingPong.java | 2 +- .../io/rsocket/transport/netty/TcpPing.java | 69 ++++++++++++++++--- .../transport/netty/TcpPongServer.java | 15 +++- .../transport/netty/WebsocketPing.java | 2 +- 8 files changed, 120 insertions(+), 21 deletions(-) create mode 100644 rsocket-test/src/main/java/io/rsocket/test/PerfTest.java diff --git a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java index dc00d7a77..f4064e63b 100644 --- a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java @@ -226,7 +226,7 @@ private static Mono newServerRSocket() { private static Mono newServerRSocket(int sessionDurationSeconds) { return RSocketFactory.receive() .resume() - .resumeStore(t -> new InMemoryResumableFramesStore("server",100_000)) + .resumeStore(t -> new InMemoryResumableFramesStore("server", 100_000)) .resumeSessionDuration(Duration.ofSeconds(sessionDurationSeconds)) .acceptor((setupPayload, rSocket) -> Mono.just(new TestResponderRSocket())) .transport(serverTransport(SERVER_HOST, SERVER_PORT)) @@ -239,14 +239,17 @@ private static class TestResponderRSocket extends AbstractRSocket { @Override public Flux requestChannel(Publisher payloads) { - return duplicate(Flux.interval(Duration.ofMillis(1)) - .onBackpressureLatest().publishOn(Schedulers.elastic()), 20) + return duplicate( + Flux.interval(Duration.ofMillis(1)) + .onBackpressureLatest() + .publishOn(Schedulers.elastic()), + 20) .map(v -> DefaultPayload.create(String.valueOf(counter.getAndIncrement()))) .takeUntilOther(Flux.from(payloads).then()); } private Flux duplicate(Flux f, int n) { - Flux r =Flux.empty(); + Flux r = Flux.empty(); for (int i = 0; i < n; i++) { r = r.mergeWith(f); } diff --git a/rsocket-test/src/main/java/io/rsocket/test/PerfTest.java b/rsocket-test/src/main/java/io/rsocket/test/PerfTest.java new file mode 100644 index 000000000..3830ec1bc --- /dev/null +++ b/rsocket-test/src/main/java/io/rsocket/test/PerfTest.java @@ -0,0 +1,17 @@ +package io.rsocket.test; + +import java.lang.annotation.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable; + +/** + * {@code @PerfTest} is used to signal that the annotated test class or method is performance test, + * and is disabled unless enabled via setting the {@code TEST_PERF_ENABLED} environment variable to + * {@code true}. + */ +@Target({ElementType.TYPE, ElementType.METHOD}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@EnabledIfEnvironmentVariable(named = "TEST_PERF_ENABLED", matches = "(?i)true") +@Test +public @interface PerfTest {} diff --git a/rsocket-test/src/main/java/io/rsocket/test/PingClient.java b/rsocket-test/src/main/java/io/rsocket/test/PingClient.java index d1ff04c79..9017e854b 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/PingClient.java +++ b/rsocket-test/src/main/java/io/rsocket/test/PingClient.java @@ -20,7 +20,9 @@ import io.rsocket.RSocket; import io.rsocket.util.ByteBufPayload; import java.time.Duration; +import java.util.function.BiFunction; import org.HdrHistogram.Recorder; +import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -49,7 +51,18 @@ public Recorder startTracker(Duration interval) { return histogram; } - public Flux startPingPong(int count, final Recorder histogram) { + public Flux requestResponsePingPong(int count, final Recorder histogram) { + return pingPong(RSocket::requestResponse, count, histogram); + } + + public Flux requestStreamPingPong(int count, final Recorder histogram) { + return pingPong(RSocket::requestStream, count, histogram); + } + + Flux pingPong( + BiFunction> interaction, + int count, + final Recorder histogram) { return client .flatMapMany( rsocket -> @@ -57,8 +70,7 @@ public Flux startPingPong(int count, final Recorder histogram) { .flatMap( i -> { long start = System.nanoTime(); - return rsocket - .requestResponse(payload.retain()) + return Flux.from(interaction.apply(rsocket, payload.retain())) .doOnNext(Payload::release) .doFinally( signalType -> { diff --git a/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java b/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java index 2f54ddb50..902014e7f 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java +++ b/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java @@ -23,6 +23,7 @@ import io.rsocket.SocketAcceptor; import io.rsocket.util.ByteBufPayload; import java.util.concurrent.ThreadLocalRandom; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class PingHandler implements SocketAcceptor { @@ -48,6 +49,12 @@ public Mono requestResponse(Payload payload) { payload.release(); return Mono.just(pong.retain()); } + + @Override + public Flux requestStream(Payload payload) { + payload.release(); + return Flux.range(0, 100).map(v -> pong.retain()); + } }); } } diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalPingPong.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalPingPong.java index 58a287948..2e4f93ac4 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalPingPong.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalPingPong.java @@ -48,7 +48,7 @@ public static void main(String... args) { int count = 1_000_000_000; pingClient - .startPingPong(count, recorder) + .requestResponsePingPong(count, recorder) .doOnTerminate(() -> System.out.println("Sent " + count + " messages.")) .blockLast(); } diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPing.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPing.java index 719c8e2cf..c2e136635 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPing.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPing.java @@ -19,30 +19,79 @@ import io.rsocket.RSocket; import io.rsocket.RSocketFactory; import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.test.PerfTest; import io.rsocket.test.PingClient; import io.rsocket.transport.netty.client.TcpClientTransport; 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 TcpPing { + private static final int INTERACTIONS_COUNT = 1_000_000_000; + private static final int port = Integer.valueOf(System.getProperty("RSOCKET_TEST_PORT", "7878")); - public static void main(String... args) { - Mono client = - RSocketFactory.connect() - .frameDecoder(PayloadDecoder.ZERO_COPY) - .transport(TcpClientTransport.create(7878)) - .start(); + @BeforeEach + void setUp() { + System.out.println("Starting ping-pong test (TCP transport)"); + System.out.println("port: " + port); + } - PingClient pingClient = new PingClient(client); + @Test + void requestResponseTest() { + 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() { + PingClient pingClient = newPingClient(); Recorder recorder = pingClient.startTracker(Duration.ofSeconds(1)); - int count = 1_000_000_000; + pingClient + .requestStreamPingPong(INTERACTIONS_COUNT, recorder) + .doOnTerminate(() -> System.out.println("Sent " + INTERACTIONS_COUNT + " messages.")) + .blockLast(); + } + + @Test + void requestStreamResumableTest() { + PingClient pingClient = newResumablePingClient(); + Recorder recorder = pingClient.startTracker(Duration.ofSeconds(1)); pingClient - .startPingPong(count, recorder) - .doOnTerminate(() -> System.out.println("Sent " + count + " messages.")) + .requestStreamPingPong(INTERACTIONS_COUNT, recorder) + .doOnTerminate(() -> System.out.println("Sent " + INTERACTIONS_COUNT + " messages.")) .blockLast(); } + + private static PingClient newPingClient() { + return newPingClient(false); + } + + private static PingClient newResumablePingClient() { + return newPingClient(true); + } + + private static PingClient newPingClient(boolean isResumable) { + RSocketFactory.ClientRSocketFactory clientRSocketFactory = RSocketFactory.connect(); + if (isResumable) { + clientRSocketFactory.resume(); + } + Mono rSocket = + clientRSocketFactory + .frameDecoder(PayloadDecoder.ZERO_COPY) + .keepAlive(Duration.ofMinutes(1), Duration.ofMinutes(30), 3) + .transport(TcpClientTransport.create(port)) + .start(); + + return new PingClient(rSocket); + } } diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java index ef5f6dbc0..53b164247 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java @@ -22,12 +22,23 @@ import io.rsocket.transport.netty.server.TcpServerTransport; public final class TcpPongServer { + private static final boolean isResume = + Boolean.valueOf(System.getProperty("RSOCKET_TEST_RESUME", "false")); + private static final int port = Integer.valueOf(System.getProperty("RSOCKET_TEST_PORT", "7878")); public static void main(String... args) { - RSocketFactory.receive() + System.out.println("Starting TCP ping-pong server"); + System.out.println("port: " + port); + System.out.println("resume enabled: " + isResume); + + RSocketFactory.ServerRSocketFactory serverRSocketFactory = RSocketFactory.receive(); + if (isResume) { + serverRSocketFactory.resume(); + } + serverRSocketFactory .frameDecoder(PayloadDecoder.ZERO_COPY) .acceptor(new PingHandler()) - .transport(TcpServerTransport.create(7878)) + .transport(TcpServerTransport.create(port)) .start() .block() .onClose() diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java index 9b03d1fe2..879df959b 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java @@ -37,7 +37,7 @@ public static void main(String... args) { int count = 1_000_000_000; pingClient - .startPingPong(count, recorder) + .requestResponsePingPong(count, recorder) .doOnTerminate(() -> System.out.println("Sent " + count + " messages.")) .blockLast(); } From 3072d2e769e14db021d49d6800c485f14ebf21b5 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 27 Mar 2019 20:09:34 +0100 Subject: [PATCH 017/181] fixes onClose issue (#608) Signed-off-by: Robert Roeser Signed-off-by: Oleh Dokuka --- .../internal/BaseDuplexConnection.java | 30 +++++++++++++ .../transport/netty/TcpDuplexConnection.java | 44 ++++++------------- .../netty/WebsocketDuplexConnection.java | 44 ++++++------------- 3 files changed, 56 insertions(+), 62 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java diff --git a/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java new file mode 100644 index 000000000..9668e5e18 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/BaseDuplexConnection.java @@ -0,0 +1,30 @@ +package io.rsocket.internal; + +import io.rsocket.DuplexConnection; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; + +public abstract class BaseDuplexConnection implements DuplexConnection { + private MonoProcessor onClose = MonoProcessor.create(); + + public BaseDuplexConnection() { + onClose.doFinally(s -> doOnClose()).subscribe(); + } + + protected abstract void doOnClose(); + + @Override + public final Mono onClose() { + return onClose; + } + + @Override + public final void dispose() { + onClose.onComplete(); + } + + @Override + public final boolean isDisposed() { + return onClose.isDisposed(); + } +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java index 9b2e60d5c..ffa5503c8 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java @@ -20,20 +20,18 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.internal.BaseDuplexConnection; import java.util.Objects; import org.reactivestreams.Publisher; -import reactor.core.Disposable; import reactor.core.Fuseable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty.Connection; -import reactor.netty.FutureMono; /** An implementation of {@link DuplexConnection} that connects via TCP. */ -public final class TcpDuplexConnection implements DuplexConnection { +public final class TcpDuplexConnection extends BaseDuplexConnection { private final Connection connection; - private final Disposable channelClosed; private final ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; private final boolean encodeLength; @@ -55,37 +53,21 @@ public TcpDuplexConnection(Connection connection) { public TcpDuplexConnection(Connection connection, boolean encodeLength) { this.encodeLength = encodeLength; this.connection = Objects.requireNonNull(connection, "connection must not be null"); - this.channelClosed = - FutureMono.from(connection.channel().closeFuture()) - .doFinally( - s -> { - if (!isDisposed()) { - dispose(); - } - }) - .subscribe(); - } - @Override - public void dispose() { - connection.dispose(); - } - - @Override - public boolean isDisposed() { - return connection.isDisposed(); + connection + .channel() + .closeFuture() + .addListener( + future -> { + if (!isDisposed()) dispose(); + }); } @Override - public Mono onClose() { - return connection - .onDispose() - .doFinally( - s -> { - if (!channelClosed.isDisposed()) { - channelClosed.dispose(); - } - }); + protected void doOnClose() { + if (!connection.isDisposed()) { + connection.dispose(); + } } @Override diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java index aa94aa0bb..f83725e47 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java @@ -18,14 +18,13 @@ import io.netty.buffer.ByteBuf; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.rsocket.DuplexConnection; +import io.rsocket.internal.BaseDuplexConnection; import java.util.Objects; import org.reactivestreams.Publisher; -import reactor.core.Disposable; import reactor.core.Fuseable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty.Connection; -import reactor.netty.FutureMono; import reactor.util.concurrent.Queues; /** @@ -35,10 +34,9 @@ * for message oriented transports so this must be specifically dropped from Frames sent and * stitched back on for frames received. */ -public final class WebsocketDuplexConnection implements DuplexConnection { +public final class WebsocketDuplexConnection extends BaseDuplexConnection { private final Connection connection; - private final Disposable channelClosed; /** * Creates a new instance @@ -47,37 +45,21 @@ public final class WebsocketDuplexConnection implements DuplexConnection { */ public WebsocketDuplexConnection(Connection connection) { this.connection = Objects.requireNonNull(connection, "connection must not be null"); - this.channelClosed = - FutureMono.from(connection.channel().closeFuture()) - .doFinally( - s -> { - if (!isDisposed()) { - dispose(); - } - }) - .subscribe(); - } - - @Override - public void dispose() { - connection.dispose(); - } - @Override - public boolean isDisposed() { - return connection.isDisposed(); + connection + .channel() + .closeFuture() + .addListener( + future -> { + if (!isDisposed()) dispose(); + }); } @Override - public Mono onClose() { - return connection - .onDispose() - .doFinally( - s -> { - if (!channelClosed.isDisposed()) { - channelClosed.dispose(); - } - }); + protected void doOnClose() { + if (!connection.isDisposed()) { + connection.dispose(); + } } @Override From ab46669840a1fc6d0b28b57217a4b1d6ff118a49 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 2 Apr 2019 10:05:17 +0300 Subject: [PATCH 018/181] fixes bug with incorrect WS framesize setup. (#610) * fixes buf with incorrect WS framesize setup. This PR provides a websocket transport's frame size setup with regards of current RSocket fragment size. Current implementation provides the following setup rules: * if `mtu` is 0 (means no fragmentation) - then WS frame size is equal to maximum default frame size of RSocket frame which is `16_777_215`; * if `mtu` is GT > 0 and LT < 65536 (default for WS frame size) then the WS frame size will be its the default one (which is `65536`); * if `mtu` is GT > 65536 then the WS frame size will be set to the specified by that parameter size Signed-off-by: Oleh Dokuka * rollbacks commented docs build * fixes format Signed-off-by: Oleh Dokuka * cleanup pollution uses max frame size from `FrameLengthFlyweight` Signed-off-by: Oleh Dokuka * simplifies logic of max frame size setup for ws transport Signed-off-by: Oleh Dokuka * fixes google java format Signed-off-by: Oleh Dokuka --- build.gradle | 8 +- .../main/java/io/rsocket/frame/FrameUtil.java | 1 + rsocket-transport-netty/build.gradle | 2 + .../client/WebsocketClientTransport.java | 4 +- .../netty/server/WebsocketRouteTransport.java | 136 +++++++++++++++++- .../server/WebsocketServerTransport.java | 4 + .../client/WebsocketClientTransportTest.java | 50 +++++++ .../server/WebsocketRouteTransportTest.java | 77 ++++++++++ .../server/WebsocketServerTransportTest.java | 70 +++++++++ .../org.mockito.plugins.MockMaker | 1 + 10 files changed, 346 insertions(+), 7 deletions(-) create mode 100644 rsocket-transport-netty/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/build.gradle b/build.gradle index d9b54696e..bf9901506 100644 --- a/build.gradle +++ b/build.gradle @@ -35,7 +35,7 @@ subprojects { ext['netty.version'] = '4.1.31.Final' ext['netty-boringssl.version'] = '2.0.18.Final' ext['hdrhistogram.version'] = '2.1.10' - ext['mockito.version'] = '2.23.0' + ext['mockito.version'] = '2.25.1' ext['slf4j.version'] = '1.7.25' ext['jmh.version'] = '1.21' ext['junit.version'] = '5.1.0' @@ -60,8 +60,12 @@ subprojects { dependency "io.micrometer:micrometer-core:${ext['micrometer.version']}" dependency "org.assertj:assertj-core:${ext['assertj.version']}" dependency "org.hdrhistogram:HdrHistogram:${ext['hdrhistogram.version']}" - dependency "org.mockito:mockito-core:${ ext['mockito.version']}" dependency "org.slf4j:slf4j-api:${ext['slf4j.version']}" + + dependencySet(group: 'org.mockito', version: ext['mockito.version']) { + entry 'mockito-junit-jupiter' + entry 'mockito-core' + } dependencySet(group: 'org.junit.jupiter', version: ext['junit.version']) { entry 'junit-jupiter-api' diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java index 43cd821df..417e44857 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java @@ -5,6 +5,7 @@ import io.netty.buffer.Unpooled; public class FrameUtil { + private FrameUtil() {} public static String toString(ByteBuf frame) { diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 5865c3756..6f706ae72 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -36,6 +36,8 @@ dependencies { 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' 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 fa364ef53..002a2e13c 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 @@ -16,6 +16,7 @@ package io.rsocket.transport.netty.client; +import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; import static io.rsocket.transport.netty.UriUtils.getPort; import static io.rsocket.transport.netty.UriUtils.isSecure; @@ -42,6 +43,7 @@ */ public final class WebsocketClientTransport implements ClientTransport, TransportHeaderAware { + private static final int DEFAULT_FRAME_SIZE = 65536; private static final String DEFAULT_PATH = "/"; private final HttpClient client; @@ -151,7 +153,7 @@ private static TcpClient createClient(URI uri) { public Mono connect(int mtu) { return client .headers(headers -> transportHeaders.get().forEach(headers::set)) - .websocket() + .websocket(FRAME_LENGTH_MASK) .uri(path) .connect() .map( diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java index fc2ab28bb..bc1b46c33 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java @@ -16,14 +16,23 @@ package io.rsocket.transport.netty.server; +import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; + import io.netty.buffer.ByteBufAllocator; +import io.netty.handler.codec.http.HttpMethod; import io.rsocket.Closeable; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.WebsocketDuplexConnection; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.function.Consumer; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import reactor.core.publisher.Mono; import reactor.netty.Connection; import reactor.netty.http.server.HttpServer; @@ -35,7 +44,7 @@ */ public final class WebsocketRouteTransport implements ServerTransport { - private final String path; + private final UriPathTemplate template; private final Consumer routesBuilder; @@ -53,7 +62,7 @@ public WebsocketRouteTransport( this.server = Objects.requireNonNull(server, "server must not be null"); this.routesBuilder = Objects.requireNonNull(routesBuilder, "routesBuilder must not be null"); - this.path = Objects.requireNonNull(path, "path must not be null"); + this.template = new UriPathTemplate(Objects.requireNonNull(path, "path must not be null")); } @Override @@ -65,7 +74,7 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { routes -> { routesBuilder.accept(routes); routes.ws( - path, + hsr -> hsr.method().equals(HttpMethod.GET) && template.matches(hsr.uri()), (in, out) -> { DuplexConnection connection = new WebsocketDuplexConnection((Connection) in); if (mtu > 0) { @@ -74,9 +83,128 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { connection, ByteBufAllocator.DEFAULT, mtu, false); } return acceptor.apply(connection).then(out.neverComplete()); - }); + }, + null, + FRAME_LENGTH_MASK); }) .bind() .map(CloseableChannel::new); } + + static final class UriPathTemplate { + + private static final Pattern FULL_SPLAT_PATTERN = Pattern.compile("[\\*][\\*]"); + private static final String FULL_SPLAT_REPLACEMENT = ".*"; + + private static final Pattern NAME_SPLAT_PATTERN = Pattern.compile("\\{([^/]+?)\\}[\\*][\\*]"); + private static final String NAME_SPLAT_REPLACEMENT = "(?<%NAME%>.*)"; + + private static final Pattern NAME_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); + private static final String NAME_REPLACEMENT = "(?<%NAME%>[^\\/]*)"; + + private final List pathVariables = new ArrayList<>(); + private final HashMap matchers = new HashMap<>(); + private final HashMap> vars = new HashMap<>(); + + private final Pattern uriPattern; + + static String filterQueryParams(String uri) { + int hasQuery = uri.lastIndexOf("?"); + if (hasQuery != -1) { + return uri.substring(0, hasQuery); + } else { + return uri; + } + } + + /** + * Creates a new {@code UriPathTemplate} from the given {@code uriPattern}. + * + * @param uriPattern The pattern to be used by the template + */ + UriPathTemplate(String uriPattern) { + String s = "^" + filterQueryParams(uriPattern); + + Matcher m = NAME_SPLAT_PATTERN.matcher(s); + while (m.find()) { + for (int i = 1; i <= m.groupCount(); i++) { + String name = m.group(i); + pathVariables.add(name); + s = m.replaceFirst(NAME_SPLAT_REPLACEMENT.replaceAll("%NAME%", name)); + m.reset(s); + } + } + + m = NAME_PATTERN.matcher(s); + while (m.find()) { + for (int i = 1; i <= m.groupCount(); i++) { + String name = m.group(i); + pathVariables.add(name); + s = m.replaceFirst(NAME_REPLACEMENT.replaceAll("%NAME%", name)); + m.reset(s); + } + } + + m = FULL_SPLAT_PATTERN.matcher(s); + while (m.find()) { + s = m.replaceAll(FULL_SPLAT_REPLACEMENT); + m.reset(s); + } + + this.uriPattern = Pattern.compile(s + "$"); + } + + /** + * Tests the given {@code uri} against this template, returning {@code true} if the uri matches + * the template, {@code false} otherwise. + * + * @param uri The uri to match + * @return {@code true} if there's a match, {@code false} otherwise + */ + public boolean matches(String uri) { + return matcher(uri).matches(); + } + + /** + * Matches the template against the given {@code uri} returning a map of path parameters + * extracted from the uri, keyed by the names in the template. If the uri does not match, or + * there are no path parameters, an empty map is returned. + * + * @param uri The uri to match + * @return the path parameters from the uri. Never {@code null}. + */ + final Map match(String uri) { + Map pathParameters = vars.get(uri); + if (null != pathParameters) { + return pathParameters; + } + + pathParameters = new HashMap<>(); + Matcher m = matcher(uri); + if (m.matches()) { + int i = 1; + for (String name : pathVariables) { + String val = m.group(i++); + pathParameters.put(name, val); + } + } + synchronized (vars) { + vars.put(uri, pathParameters); + } + + return pathParameters; + } + + private Matcher matcher(String uri) { + uri = filterQueryParams(uri); + Matcher m = matchers.get(uri); + if (null == m) { + m = uriPattern.matcher(uri); + synchronized (matchers) { + matchers.put(uri, m); + } + } + return m; + } + } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java index 9e1af2395..8e9ba377d 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java @@ -16,6 +16,8 @@ package io.rsocket.transport.netty.server; +import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; + import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; @@ -114,6 +116,8 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { (request, response) -> { transportHeaders.get().forEach(response::addHeader); return response.sendWebsocket( + null, + FRAME_LENGTH_MASK, (in, out) -> { DuplexConnection connection = new WebsocketDuplexConnection((Connection) in); if (mtu > 0) { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java index b2aba0235..9d989d769 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java @@ -16,6 +16,7 @@ package io.rsocket.transport.netty.client; +import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; @@ -23,14 +24,63 @@ import java.net.InetSocketAddress; import java.net.URI; import java.util.Collections; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; import reactor.core.publisher.Mono; import reactor.netty.http.client.HttpClient; import reactor.test.StepVerifier; +@ExtendWith(MockitoExtension.class) final class WebsocketClientTransportTest { + @Test + public void testThatSetupWithUnSpecifiedFrameSizeShouldSetMaxFrameSize() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Integer.class); + HttpClient httpClient = Mockito.spy(HttpClient.create()); + Mockito.doAnswer(a -> httpClient).when(httpClient).headers(Mockito.any()); + Mockito.doCallRealMethod().when(httpClient).websocket(captor.capture()); + + WebsocketClientTransport clientTransport = WebsocketClientTransport.create(httpClient, ""); + + clientTransport.connect(0).subscribe(); + + Assertions.assertThat(captor.getValue()).isEqualTo(FRAME_LENGTH_MASK); + } + + @Test + public void testThatSetupWithSpecifiedFrameSizeButLowerThanWsDefaultShouldSetToWsDefault() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Integer.class); + HttpClient httpClient = Mockito.spy(HttpClient.create()); + Mockito.doAnswer(a -> httpClient).when(httpClient).headers(Mockito.any()); + Mockito.doCallRealMethod().when(httpClient).websocket(captor.capture()); + + WebsocketClientTransport clientTransport = WebsocketClientTransport.create(httpClient, ""); + + clientTransport.connect(65536 - 10000).subscribe(); + + Assertions.assertThat(captor.getValue()).isEqualTo(FRAME_LENGTH_MASK); + } + + @Test + public void + testThatSetupWithSpecifiedFrameSizeButHigherThanWsDefaultShouldSetToSpecifiedFrameSize() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Integer.class); + HttpClient httpClient = Mockito.spy(HttpClient.create()); + Mockito.doAnswer(a -> httpClient).when(httpClient).headers(Mockito.any()); + Mockito.doCallRealMethod().when(httpClient).websocket(captor.capture()); + + WebsocketClientTransport clientTransport = WebsocketClientTransport.create(httpClient, ""); + + clientTransport.connect(65536 + 10000).subscribe(); + + Assertions.assertThat(captor.getValue()).isEqualTo(FRAME_LENGTH_MASK); + } + @DisplayName("connects to server") @Test void connect() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java index e94bef13c..26f598c2d 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java @@ -16,16 +16,93 @@ package io.rsocket.transport.netty.server; +import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; import static org.assertj.core.api.Assertions.assertThatNullPointerException; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Predicate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; import reactor.core.publisher.Mono; import reactor.netty.http.server.HttpServer; +import reactor.netty.http.server.HttpServerRoutes; import reactor.test.StepVerifier; final class WebsocketRouteTransportTest { + @Test + public void testThatSetupWithUnSpecifiedFrameSizeShouldSetMaxFrameSize() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Consumer.class); + HttpServer httpServer = Mockito.spy(HttpServer.create()); + HttpServerRoutes routes = Mockito.mock(HttpServerRoutes.class); + Mockito.doAnswer(a -> httpServer).when(httpServer).route(captor.capture()); + Mockito.doAnswer(a -> Mono.empty()).when(httpServer).bind(); + + WebsocketRouteTransport serverTransport = + new WebsocketRouteTransport(httpServer, (r) -> {}, ""); + + serverTransport.start(c -> Mono.empty(), 0).subscribe(); + + captor.getValue().accept(routes); + + Mockito.verify(routes) + .ws( + Mockito.any(Predicate.class), + Mockito.any(BiFunction.class), + Mockito.nullable(String.class), + Mockito.eq(FRAME_LENGTH_MASK)); + } + + @Test + public void testThatSetupWithSpecifiedFrameSizeButLowerThanWsDefaultShouldSetToWsDefault() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Consumer.class); + HttpServer httpServer = Mockito.spy(HttpServer.create()); + HttpServerRoutes routes = Mockito.mock(HttpServerRoutes.class); + Mockito.doAnswer(a -> httpServer).when(httpServer).route(captor.capture()); + Mockito.doAnswer(a -> Mono.empty()).when(httpServer).bind(); + + WebsocketRouteTransport serverTransport = + new WebsocketRouteTransport(httpServer, (r) -> {}, ""); + + serverTransport.start(c -> Mono.empty(), 1000).subscribe(); + + captor.getValue().accept(routes); + + Mockito.verify(routes) + .ws( + Mockito.any(Predicate.class), + Mockito.any(BiFunction.class), + Mockito.nullable(String.class), + Mockito.eq(FRAME_LENGTH_MASK)); + } + + @Test + public void + testThatSetupWithSpecifiedFrameSizeButHigherThanWsDefaultShouldSetToSpecifiedFrameSize() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Consumer.class); + HttpServer httpServer = Mockito.spy(HttpServer.create()); + HttpServerRoutes routes = Mockito.mock(HttpServerRoutes.class); + Mockito.doAnswer(a -> httpServer).when(httpServer).route(captor.capture()); + Mockito.doAnswer(a -> Mono.empty()).when(httpServer).bind(); + + WebsocketRouteTransport serverTransport = + new WebsocketRouteTransport(httpServer, (r) -> {}, ""); + + serverTransport.start(c -> Mono.empty(), 65536 + 1000).subscribe(); + + captor.getValue().accept(routes); + + Mockito.verify(routes) + .ws( + Mockito.any(Predicate.class), + Mockito.any(BiFunction.class), + Mockito.nullable(String.class), + Mockito.eq(FRAME_LENGTH_MASK)); + } + @DisplayName("creates server") @Test void constructor() { diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java index 7a5e360d2..b1f5e647d 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java @@ -16,19 +16,89 @@ package io.rsocket.transport.netty.server; +import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import java.net.InetSocketAddress; import java.util.Collections; +import java.util.function.BiFunction; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; import reactor.core.publisher.Mono; import reactor.netty.http.server.HttpServer; +import reactor.netty.http.server.HttpServerRequest; +import reactor.netty.http.server.HttpServerResponse; import reactor.test.StepVerifier; final class WebsocketServerTransportTest { + @Test + public void testThatSetupWithUnSpecifiedFrameSizeShouldSetMaxFrameSize() { + ArgumentCaptor captor = ArgumentCaptor.forClass(BiFunction.class); + HttpServer httpServer = Mockito.spy(HttpServer.create()); + Mockito.doAnswer(a -> httpServer).when(httpServer).handle(captor.capture()); + Mockito.doAnswer(a -> Mono.empty()).when(httpServer).bind(); + + WebsocketServerTransport serverTransport = WebsocketServerTransport.create(httpServer); + + serverTransport.start(c -> Mono.empty(), 0).subscribe(); + + HttpServerRequest httpServerRequest = Mockito.mock(HttpServerRequest.class); + HttpServerResponse httpServerResponse = Mockito.mock(HttpServerResponse.class); + + captor.getValue().apply(httpServerRequest, httpServerResponse); + + Mockito.verify(httpServerResponse) + .sendWebsocket( + Mockito.nullable(String.class), Mockito.eq(FRAME_LENGTH_MASK), Mockito.any()); + } + + @Test + public void testThatSetupWithSpecifiedFrameSizeButLowerThanWsDefaultShouldSetToWsDefault() { + ArgumentCaptor captor = ArgumentCaptor.forClass(BiFunction.class); + HttpServer httpServer = Mockito.spy(HttpServer.create()); + Mockito.doAnswer(a -> httpServer).when(httpServer).handle(captor.capture()); + Mockito.doAnswer(a -> Mono.empty()).when(httpServer).bind(); + + WebsocketServerTransport serverTransport = WebsocketServerTransport.create(httpServer); + + serverTransport.start(c -> Mono.empty(), 1000).subscribe(); + + HttpServerRequest httpServerRequest = Mockito.mock(HttpServerRequest.class); + HttpServerResponse httpServerResponse = Mockito.mock(HttpServerResponse.class); + + captor.getValue().apply(httpServerRequest, httpServerResponse); + + Mockito.verify(httpServerResponse) + .sendWebsocket( + Mockito.nullable(String.class), Mockito.eq(FRAME_LENGTH_MASK), Mockito.any()); + } + + @Test + public void + testThatSetupWithSpecifiedFrameSizeButHigherThanWsDefaultShouldSetToSpecifiedFrameSize() { + ArgumentCaptor captor = ArgumentCaptor.forClass(BiFunction.class); + HttpServer httpServer = Mockito.spy(HttpServer.create()); + Mockito.doAnswer(a -> httpServer).when(httpServer).handle(captor.capture()); + Mockito.doAnswer(a -> Mono.empty()).when(httpServer).bind(); + + WebsocketServerTransport serverTransport = WebsocketServerTransport.create(httpServer); + + serverTransport.start(c -> Mono.empty(), 65536 + 1000).subscribe(); + + HttpServerRequest httpServerRequest = Mockito.mock(HttpServerRequest.class); + HttpServerResponse httpServerResponse = Mockito.mock(HttpServerResponse.class); + + captor.getValue().apply(httpServerRequest, httpServerResponse); + + Mockito.verify(httpServerResponse) + .sendWebsocket( + Mockito.nullable(String.class), Mockito.eq(FRAME_LENGTH_MASK), Mockito.any()); + } + @DisplayName("creates server with BindAddress") @Test void createBindAddress() { diff --git a/rsocket-transport-netty/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/rsocket-transport-netty/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 000000000..ca6ee9cea --- /dev/null +++ b/rsocket-transport-netty/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file From b4559471718b3af82af105b57a69470f201734a2 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 5 Apr 2019 00:29:14 +0300 Subject: [PATCH 019/181] Feature/external httpserver (#612) * fixes buf with incorrect WS framesize setup. This PR provides a websocket transport's frame size setup with regards of current RSocket fragment size. Current implementation provides the following setup rules: * if `mtu` is 0 (means no fragmentation) - then WS frame size is equal to maximum default frame size of RSocket frame which is `16_777_215`; * if `mtu` is GT > 0 and LT < 65536 (default for WS frame size) then the WS frame size will be its the default one (which is `65536`); * if `mtu` is GT > 65536 then the WS frame size will be set to the specified by that parameter size Signed-off-by: Oleh Dokuka * exposes access to connection acceptor (#600) Signed-off-by: Oleh Dokuka --- .../main/java/io/rsocket/RSocketFactory.java | 203 +++++++++--------- .../netty/server/WebsocketRouteTransport.java | 46 +++- .../WebSocketTransportIntegrationTest.java | 60 ++++++ 3 files changed, 204 insertions(+), 105 deletions(-) create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index e3ea1a25e..892aaa1ab 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -77,6 +77,9 @@ default Start transport(ClientTransport transport) { } public interface ServerTransportAcceptor { + + ServerTransport.ConnectionAcceptor toConnectionAcceptor(); + Start transport(Supplier> transport); default Start transport(ServerTransport transport) { @@ -385,7 +388,7 @@ public ServerRSocketFactory addServerPlugin(RSocketInterceptor interceptor) { public ServerTransportAcceptor acceptor(SocketAcceptor acceptor) { this.acceptor = acceptor; - return ServerStart::new; + return new ServerStart<>(); } public ServerRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { @@ -424,11 +427,109 @@ public ServerRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout) { return this; } - private class ServerStart implements Start { - private final Supplier> transportServer; + private class ServerStart implements Start, ServerTransportAcceptor { + private Supplier> transportServer; - ServerStart(Supplier> transportServer) { - this.transportServer = transportServer; + @Override + public ServerTransport.ConnectionAcceptor toConnectionAcceptor() { + return new ServerTransport.ConnectionAcceptor() { + private final ServerSetup serverSetup = serverSetup(); + + @Override + public Mono apply(DuplexConnection connection) { + return acceptor(serverSetup, connection); + } + }; + } + + @Override + @SuppressWarnings("unchecked") + public Start transport(Supplier> transport) { + this.transportServer = (Supplier) transport; + return (Start) this::start; + } + + private Mono acceptor(ServerSetup serverSetup, DuplexConnection connection) { + connection = + KeepAliveConnection.ofServer( + allocator, connection, serverSetup.keepAliveData(), errorConsumer); + ClientServerInputMultiplexer multiplexer = + new ClientServerInputMultiplexer(connection, plugins); + + return multiplexer + .asSetupConnection() + .receive() + .next() + .flatMap(startFrame -> accept(serverSetup, startFrame, multiplexer)); + } + + private Mono acceptResume( + ServerSetup serverSetup, ByteBuf resumeFrame, ClientServerInputMultiplexer multiplexer) { + return serverSetup.acceptRSocketResume(resumeFrame, multiplexer); + } + + private Mono accept( + ServerSetup serverSetup, ByteBuf startFrame, ClientServerInputMultiplexer multiplexer) { + switch (FrameHeaderFlyweight.frameType(startFrame)) { + case SETUP: + return acceptSetup(serverSetup, startFrame, multiplexer); + case RESUME: + return acceptResume(serverSetup, startFrame, multiplexer); + default: + return acceptUnknown(startFrame, multiplexer); + } + } + + private Mono acceptSetup( + ServerSetup serverSetup, ByteBuf setupFrame, ClientServerInputMultiplexer multiplexer) { + + if (!SetupFrameFlyweight.isSupportedVersion(setupFrame)) { + return sendError( + multiplexer, + new InvalidSetupException( + "Unsupported version: " + + SetupFrameFlyweight.humanReadableVersion(setupFrame))) + .doFinally( + signalType -> { + setupFrame.release(); + multiplexer.dispose(); + }); + } + return serverSetup.acceptRSocketSetup( + setupFrame, + multiplexer, + wrappedMultiplexer -> { + ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(setupFrame); + + RSocketClient rSocketClient = + new RSocketClient( + allocator, + wrappedMultiplexer.asServerConnection(), + payloadDecoder, + errorConsumer, + StreamIdSupplier.serverSupplier()); + + RSocket wrappedRSocketClient = plugins.applyClient(rSocketClient); + + return acceptor + .accept(setupPayload, wrappedRSocketClient) + .onErrorResume( + err -> sendError(multiplexer, rejectedSetupError(err)).then(Mono.error(err))) + .doOnNext( + unwrappedServerSocket -> { + RSocket wrappedRSocketServer = plugins.applyServer(unwrappedServerSocket); + + RSocketServer rSocketServer = + new RSocketServer( + allocator, + wrappedMultiplexer.asClientConnection(), + wrappedRSocketServer, + payloadDecoder, + errorConsumer); + }) + .doFinally(signalType -> setupPayload.release()) + .then(); + }); } @Override @@ -442,99 +543,9 @@ public Mono start() { public Mono get() { return transportServer .get() - .start( - connection -> { - connection = - KeepAliveConnection.ofServer( - allocator, - connection, - serverSetup.keepAliveData(), - errorConsumer); - ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(connection, plugins); - - return multiplexer - .asSetupConnection() - .receive() - .next() - .flatMap(startFrame -> accept(startFrame, multiplexer)); - }, - mtu) + .start(duplexConnection -> acceptor(serverSetup, duplexConnection), mtu) .doOnNext(c -> c.onClose().doFinally(v -> serverSetup.dispose()).subscribe()); } - - private Mono accept( - ByteBuf startFrame, ClientServerInputMultiplexer multiplexer) { - switch (FrameHeaderFlyweight.frameType(startFrame)) { - case SETUP: - return acceptSetup(startFrame, multiplexer); - case RESUME: - return acceptResume(startFrame, multiplexer); - default: - return acceptUnknown(startFrame, multiplexer); - } - } - - private Mono acceptSetup( - ByteBuf setupFrame, ClientServerInputMultiplexer multiplexer) { - - if (!SetupFrameFlyweight.isSupportedVersion(setupFrame)) { - return sendError( - multiplexer, - new InvalidSetupException( - "Unsupported version: " - + SetupFrameFlyweight.humanReadableVersion(setupFrame))) - .doFinally( - signalType -> { - setupFrame.release(); - multiplexer.dispose(); - }); - } - return serverSetup.acceptRSocketSetup( - setupFrame, - multiplexer, - wrappedMultiplexer -> { - ConnectionSetupPayload setupPayload = - ConnectionSetupPayload.create(setupFrame); - - RSocketClient rSocketClient = - new RSocketClient( - allocator, - wrappedMultiplexer.asServerConnection(), - payloadDecoder, - errorConsumer, - StreamIdSupplier.serverSupplier()); - - RSocket wrappedRSocketClient = plugins.applyClient(rSocketClient); - - return acceptor - .accept(setupPayload, wrappedRSocketClient) - .onErrorResume( - err -> - sendError(multiplexer, rejectedSetupError(err)) - .then(Mono.error(err))) - .doOnNext( - unwrappedServerSocket -> { - RSocket wrappedRSocketServer = - plugins.applyServer(unwrappedServerSocket); - - RSocketServer rSocketServer = - new RSocketServer( - allocator, - wrappedMultiplexer.asClientConnection(), - wrappedRSocketServer, - payloadDecoder, - errorConsumer); - }) - .doFinally(signalType -> setupPayload.release()) - .then(); - }); - } - - private Mono acceptResume( - ByteBuf resumeFrame, ClientServerInputMultiplexer multiplexer) { - return serverSetup.acceptRSocketResume(resumeFrame, multiplexer); - } }); } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java index bc1b46c33..8d9a3b137 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java @@ -30,13 +30,17 @@ import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import reactor.netty.Connection; import reactor.netty.http.server.HttpServer; import reactor.netty.http.server.HttpServerRoutes; +import reactor.netty.http.websocket.WebsocketInbound; +import reactor.netty.http.websocket.WebsocketOutbound; /** * An implementation of {@link ServerTransport} that connects via Websocket and listens on specified @@ -75,15 +79,7 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { routesBuilder.accept(routes); routes.ws( hsr -> hsr.method().equals(HttpMethod.GET) && template.matches(hsr.uri()), - (in, out) -> { - DuplexConnection connection = new WebsocketDuplexConnection((Connection) in); - if (mtu > 0) { - connection = - new FragmentationDuplexConnection( - connection, ByteBufAllocator.DEFAULT, mtu, false); - } - return acceptor.apply(connection).then(out.neverComplete()); - }, + newHandler(acceptor, mtu), null, FRAME_LENGTH_MASK); }) @@ -91,6 +87,38 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { .map(CloseableChannel::new); } + /** + * Creates a new Websocket handler + * + * @param acceptor the {@link ConnectionAcceptor} to use with the handler + * @return a new Websocket handler + * @throws NullPointerException if {@code acceptor} is {@code null} + */ + public static BiFunction> newHandler( + ConnectionAcceptor acceptor) { + return newHandler(acceptor, 0); + } + + /** + * Creates a new Websocket handler + * + * @param acceptor the {@link ConnectionAcceptor} to use with the handler + * @param mtu the fragment size + * @return a new Websocket handler + * @throws NullPointerException if {@code acceptor} is {@code null} + */ + public static BiFunction> newHandler( + ConnectionAcceptor acceptor, int mtu) { + return (in, out) -> { + DuplexConnection connection = new WebsocketDuplexConnection((Connection) in); + if (mtu > 0) { + connection = + new FragmentationDuplexConnection(connection, ByteBufAllocator.DEFAULT, mtu, false); + } + return acceptor.apply(connection).then(out.neverComplete()); + }; + } + static final class UriPathTemplate { private static final Pattern FULL_SPLAT_PATTERN = Pattern.compile("[\\*][\\*]"); diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java new file mode 100644 index 000000000..4fe40d232 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java @@ -0,0 +1,60 @@ +package io.rsocket.transport.netty; + +import io.rsocket.AbstractRSocket; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.RSocketFactory; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.client.WebsocketClientTransport; +import io.rsocket.transport.netty.server.WebsocketRouteTransport; +import io.rsocket.util.DefaultPayload; +import io.rsocket.util.EmptyPayload; +import java.net.URI; +import java.time.Duration; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.netty.DisposableServer; +import reactor.netty.http.server.HttpServer; +import reactor.test.StepVerifier; + +public class WebSocketTransportIntegrationTest { + + @Test + public void sendStreamOfDataWithExternalHttpServerTest() { + ServerTransport.ConnectionAcceptor acceptor = + RSocketFactory.receive() + .acceptor( + (setupPayload, sendingRSocket) -> { + return Mono.just( + new AbstractRSocket() { + @Override + public Flux requestStream(Payload payload) { + return Flux.range(0, 10) + .map(i -> DefaultPayload.create(String.valueOf(i))); + } + }); + }) + .toConnectionAcceptor(); + + DisposableServer server = + HttpServer.create() + .host("localhost") + .route(router -> router.ws("/test", WebsocketRouteTransport.newHandler(acceptor))) + .bindNow(); + + RSocket rsocket = + RSocketFactory.connect() + .transport( + WebsocketClientTransport.create( + URI.create("ws://" + server.host() + ":" + server.port() + "/test"))) + .start() + .block(); + + StepVerifier.create(rsocket.requestStream(EmptyPayload.INSTANCE)) + .expectSubscription() + .expectNextCount(10) + .expectComplete() + .verify(Duration.ofMillis(1000)); + } +} From 92be5e2a39d434272f524513d65a94454e3241da Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Sun, 7 Apr 2019 11:05:53 +0300 Subject: [PATCH 020/181] Switch resume position tracking to byte count (#611) * channel load test * Revert "General performance improvements (#598)" This reverts commit f55cf8f2 * use byte-count for frame positions Signed-off-by: Maksym Ostroverkhov * resume token is ByteBuf Signed-off-by: Maksym Ostroverkhov * Keep-alive connection, receiving remote implied position: replace Flux with simple onRemoteImpliedPosition(long) callback Signed-off-by: Maksym Ostroverkhov * better performance with resume on Signed-off-by: Maksym Ostroverkhov * keep alive handler: less allocations Signed-off-by: Maksym Ostroverkhov * remove ResumptionState to reduce allocations Signed-off-by: Maksym Ostroverkhov * remove ResumedFramesCalculator Signed-off-by: Maksym Ostroverkhov * rename ResumeAwareConnection to ResumePositionsConnection Signed-off-by: Maksym Ostroverkhov * ResumableDuplexConnection: use drain-queue for actions Signed-off-by: Maksym Ostroverkhov * tests Signed-off-by: Maksym Ostroverkhov * formatter Signed-off-by: Maksym Ostroverkhov * cleanup: resume token is released in ClientRSocketSession instead of RSocketFactory KeepAliveConnection: dispose KeepAliveHandler in onClose() Signed-off-by: Maksym Ostroverkhov * reduce allocations Signed-off-by: Maksym Ostroverkhov * resume: fix race between send / release of frames on received implied position from keepalives replace state enum with constants remove unnecessary allocation make cleanup of store on keep-alive optional formatter Signed-off-by: Maksym Ostroverkhov * remove unintended changes Signed-off-by: Maksym Ostroverkhov * optimize KeepAliveConnection optimize ResumableDuplexConnection Signed-off-by: Maksym Ostroverkhov --- .../main/java/io/rsocket/RSocketFactory.java | 45 ++-- .../rsocket/frame/FrameHeaderFlyweight.java | 13 ++ .../rsocket/frame/ResumeFrameFlyweight.java | 23 +- .../io/rsocket/frame/SetupFrameFlyweight.java | 7 +- .../internal/ClientServerConnection.java | 13 +- .../ClientServerInputMultiplexer.java | 6 +- .../java/io/rsocket/internal/ClientSetup.java | 57 +++-- .../java/io/rsocket/internal/ServerSetup.java | 109 ++++----- .../keepalive/KeepAliveConnection.java | 87 ++++---- .../rsocket/keepalive/KeepAliveHandler.java | 23 +- .../rsocket/resume/ClientRSocketSession.java | 85 ++++--- .../java/io/rsocket/resume/ClientResume.java | 7 +- .../resume/InMemoryResumableFramesStore.java | 167 ++++++++++---- .../io/rsocket/resume/RSocketSession.java | 4 +- .../resume/ResumableDuplexConnection.java | 210 +++++++++++------- .../rsocket/resume/ResumableFramesStore.java | 2 +- ...on.java => ResumePositionsConnection.java} | 5 +- .../rsocket/resume/ResumeStateException.java | 31 ++- .../io/rsocket/resume/ResumeStateHolder.java | 2 + .../java/io/rsocket/resume/ResumeToken.java | 101 --------- .../resume/ResumedFramesCalculator.java | 52 ----- .../rsocket/resume/ServerRSocketSession.java | 67 +++--- .../resume/ServerResumeConfiguration.java | 7 +- .../io/rsocket/resume/SessionManager.java | 33 ++- .../resume/UpstreamFramesSubscriber.java | 49 +--- .../test/java/io/rsocket/KeepAliveTest.java | 28 ++- .../frame/ResumeFrameFlyweightTest.java | 6 +- .../frame/SetupFrameFlyweightTest.java | 5 +- .../resume/InMemoryResumeStoreTest.java | 93 ++++++++ .../rsocket/resume/ResumeCalculatorTest.java | 48 +--- .../rsocket/resume/ResumeIntegrationTest.java | 7 +- 31 files changed, 758 insertions(+), 634 deletions(-) rename rsocket-core/src/main/java/io/rsocket/resume/{ResumeAwareConnection.java => ResumePositionsConnection.java} (79%) delete mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumeToken.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumedFramesCalculator.java create mode 100644 rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 892aaa1ab..2560ac01a 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -21,6 +21,7 @@ import io.rsocket.exceptions.InvalidSetupException; import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.ResumeFrameFlyweight; import io.rsocket.frame.SetupFrameFlyweight; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.ClientServerInputMultiplexer; @@ -106,9 +107,10 @@ public static class ClientRSocketFactory implements ClientTransportAcceptor { private String dataMimeType = "application/binary"; private boolean resumeEnabled; - private Supplier resumeTokenSupplier = ResumeToken::generate; - private Function resumeStoreFactory = - token -> new InMemoryResumableFramesStore("client", 1024); + private boolean resumeCleanupStoreOnKeepAlive; + private Supplier resumeTokenSupplier = ResumeFrameFlyweight::generateResumeToken; + private Function resumeStoreFactory = + token -> new InMemoryResumableFramesStore("client", 100_000); private Duration resumeSessionDuration = Duration.ofMinutes(2); private Duration resumeStreamTimeout = Duration.ofSeconds(10); private Supplier resumeStrategySupplier = @@ -192,13 +194,13 @@ public ClientRSocketFactory resume() { return this; } - public ClientRSocketFactory resumeToken(Supplier resumeTokenSupplier) { + public ClientRSocketFactory resumeToken(Supplier resumeTokenSupplier) { this.resumeTokenSupplier = Objects.requireNonNull(resumeTokenSupplier); return this; } public ClientRSocketFactory resumeStore( - Function resumeStoreFactory) { + Function resumeStoreFactory) { this.resumeStoreFactory = resumeStoreFactory; return this; } @@ -218,6 +220,11 @@ public ClientRSocketFactory resumeStrategy(Supplier resumeStrate return this; } + public ClientRSocketFactory resumeCleanupOnKeepAlive() { + resumeCleanupStoreOnKeepAlive = true; + return this; + } + @Override public Start transport(Supplier transportClient) { return new StartClient(transportClient); @@ -267,6 +274,7 @@ public Mono start() { connection -> { ClientSetup clientSetup = clientSetup(); DuplexConnection wrappedConnection = clientSetup.wrappedConnection(connection); + ByteBuf resumeToken = clientSetup.resumeToken(); ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(wrappedConnection, plugins); @@ -299,7 +307,7 @@ public Mono start() { false, (int) keepAliveTickPeriod(), (int) keepAliveTimeout(), - clientSetup.resumeToken().toByteBuf(), + resumeToken, metadataMimeType, dataMimeType, setupPayload.sliceMetadata(), @@ -319,7 +327,7 @@ private long keepAliveTimeout() { private ClientSetup clientSetup() { if (resumeEnabled) { - ResumeToken resumeToken = resumeTokenSupplier.get(); + ByteBuf resumeToken = resumeTokenSupplier.get(); return new ClientSetup.ResumableClientSetup( allocator, newConnection(), @@ -327,7 +335,8 @@ private ClientSetup clientSetup() { resumeStoreFactory.apply(resumeToken), resumeSessionDuration, resumeStreamTimeout, - resumeStrategySupplier); + resumeStrategySupplier, + resumeCleanupStoreOnKeepAlive); } else { return new ClientSetup.DefaultClientSetup(); } @@ -342,8 +351,7 @@ private Mono newConnection() { KeepAliveConnection.ofClient( allocator, connection, - notUsed -> - Mono.just(new KeepAliveData(keepAliveTickPeriod(), keepAliveTimeout())), + notUsed -> new KeepAliveData(keepAliveTickPeriod(), keepAliveTimeout()), errorConsumer)); } } @@ -358,10 +366,11 @@ public static class ServerRSocketFactory { private boolean resumeSupported; private Duration resumeSessionDuration = Duration.ofSeconds(120); private Duration resumeStreamTimeout = Duration.ofSeconds(10); - private Function resumeStoreFactory = - token -> new InMemoryResumableFramesStore("server", 1024); + private Function resumeStoreFactory = + token -> new InMemoryResumableFramesStore("server", 100_000); private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + private boolean resumeCleanupStoreOnKeepAlive; private ServerRSocketFactory() {} @@ -412,7 +421,7 @@ public ServerRSocketFactory resume() { } public ServerRSocketFactory resumeStore( - Function resumeStoreFactory) { + Function resumeStoreFactory) { this.resumeStoreFactory = resumeStoreFactory; return this; } @@ -427,6 +436,11 @@ public ServerRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout) { return this; } + public ServerRSocketFactory resumeCleanupOnKeepAlive() { + resumeCleanupStoreOnKeepAlive = true; + return this; + } + private class ServerStart implements Start, ServerTransportAcceptor { private Supplier> transportServer; @@ -452,7 +466,7 @@ public Start transport(Supplier> tra private Mono acceptor(ServerSetup serverSetup, DuplexConnection connection) { connection = KeepAliveConnection.ofServer( - allocator, connection, serverSetup.keepAliveData(), errorConsumer); + allocator, connection, serverSetup::keepAliveData, errorConsumer); ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(connection, plugins); @@ -556,7 +570,8 @@ private ServerSetup serverSetup() { new SessionManager(), resumeSessionDuration, resumeStreamTimeout, - resumeStoreFactory) + resumeStoreFactory, + resumeCleanupStoreOnKeepAlive) : new ServerSetup.DefaultServerSetup(allocator); } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java index 7f03984d8..cbc677444 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java @@ -79,6 +79,19 @@ public static boolean hasMetadata(ByteBuf byteBuf) { return (flags(byteBuf) & FLAGS_M) == FLAGS_M; } + /** + * faster version of {@link #frameType(ByteBuf)} which does not replace PAYLOAD with synthetic + * type + */ + public static FrameType nativeFrameType(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + byteBuf.skipBytes(Integer.BYTES); + int typeAndFlags = byteBuf.readShort() & 0xFFFF; + FrameType result = FrameType.fromEncodedType(typeAndFlags >> FRAME_TYPE_SHIFT); + byteBuf.resetReaderIndex(); + return result; + } + public static FrameType frameType(ByteBuf byteBuf) { byteBuf.markReaderIndex(); byteBuf.skipBytes(Integer.BYTES); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameFlyweight.java index 91878b44b..06c9fc38c 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameFlyweight.java @@ -18,20 +18,24 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import java.util.UUID; public class ResumeFrameFlyweight { static final int CURRENT_VERSION = SetupFrameFlyweight.CURRENT_VERSION; public static ByteBuf encode( - final ByteBufAllocator allocator, - byte[] token, + ByteBufAllocator allocator, + ByteBuf token, long lastReceivedServerPos, long firstAvailableClientPos) { ByteBuf byteBuf = FrameHeaderFlyweight.encodeStreamZero(allocator, FrameType.RESUME, 0); byteBuf.writeInt(CURRENT_VERSION); - byteBuf.writeShort(token.length); + token.markReaderIndex(); + byteBuf.writeShort(token.readableBytes()); byteBuf.writeBytes(token); + token.resetReaderIndex(); byteBuf.writeLong(lastReceivedServerPos); byteBuf.writeLong(firstAvailableClientPos); @@ -49,7 +53,7 @@ public static int version(ByteBuf byteBuf) { return version; } - public static byte[] token(ByteBuf byteBuf) { + public static ByteBuf token(ByteBuf byteBuf) { FrameHeaderFlyweight.ensureFrameType(FrameType.RESUME, byteBuf); byteBuf.markReaderIndex(); @@ -58,8 +62,7 @@ public static byte[] token(ByteBuf byteBuf) { byteBuf.skipBytes(tokenPos); // token int tokenLength = byteBuf.readShort() & 0xFFFF; - byte[] token = new byte[tokenLength]; - byteBuf.readBytes(token); + ByteBuf token = byteBuf.readSlice(tokenLength); byteBuf.resetReaderIndex(); return token; @@ -98,4 +101,12 @@ public static long firstAvailableClientPos(ByteBuf byteBuf) { return firstAvailableClientPos; } + + public static ByteBuf generateResumeToken() { + UUID uuid = UUID.randomUUID(); + ByteBuf bb = Unpooled.buffer(16); + bb.writeLong(uuid.getMostSignificantBits()); + bb.writeLong(uuid.getLeastSignificantBits()); + return bb; + } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java index fc0142561..746485067 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java @@ -76,7 +76,9 @@ public static ByteBuf encode( header.writeInt(CURRENT_VERSION).writeInt(keepaliveInterval).writeInt(maxLifetime); if ((flags & FLAGS_RESUME_ENABLE) != 0) { + resumeToken.markReaderIndex(); header.writeShort(resumeToken.readableBytes()).writeBytes(resumeToken); + resumeToken.resetReaderIndex(); } // Write metadata mime-type @@ -141,7 +143,7 @@ public static boolean resumeEnabled(ByteBuf byteBuf) { return (FLAGS_RESUME_ENABLE & FrameHeaderFlyweight.flags(byteBuf)) == FLAGS_RESUME_ENABLE; } - public static byte[] resumeToken(ByteBuf byteBuf) { + public static ByteBuf resumeToken(ByteBuf byteBuf) { if (resumeEnabled(byteBuf)) { byteBuf.markReaderIndex(); // header @@ -158,8 +160,7 @@ public static byte[] resumeToken(ByteBuf byteBuf) { Integer.BYTES; int tokenLength = byteBuf.skipBytes(resumePos).readShort() & 0xFFFF; - byte[] resumeToken = new byte[tokenLength]; - byteBuf.readBytes(resumeToken); + ByteBuf resumeToken = byteBuf.readSlice(tokenLength); byteBuf.resetReaderIndex(); return resumeToken; } else { diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerConnection.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerConnection.java index 3043e8082..1b89a6edd 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerConnection.java @@ -17,12 +17,11 @@ package io.rsocket.internal; import io.rsocket.DuplexConnection; -import io.rsocket.resume.ResumeAwareConnection; +import io.rsocket.resume.ResumePositionsConnection; import io.rsocket.resume.ResumeStateHolder; import io.rsocket.util.DuplexConnectionProxy; -import reactor.core.publisher.Flux; -class ClientServerConnection extends DuplexConnectionProxy implements ResumeAwareConnection { +class ClientServerConnection extends DuplexConnectionProxy implements ResumePositionsConnection { private final DuplexConnection resumeAware; @@ -32,9 +31,9 @@ public ClientServerConnection(DuplexConnection delegate, DuplexConnection resume } @Override - public Flux receiveResumePositions(ResumeStateHolder resumeStateHolder) { - return resumeAware instanceof ResumeAwareConnection - ? ((ResumeAwareConnection) resumeAware).receiveResumePositions(resumeStateHolder) - : Flux.never(); + public void acceptResumeState(ResumeStateHolder resumeStateHolder) { + if (resumeAware instanceof ResumePositionsConnection) { + ((ResumePositionsConnection) resumeAware).acceptResumeState(resumeStateHolder); + } } } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index e132ade40..d1a83cec5 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -23,7 +23,7 @@ import io.rsocket.frame.FrameUtil; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; import io.rsocket.plugins.PluginRegistry; -import io.rsocket.resume.ResumeAwareConnection; +import io.rsocket.resume.ResumePositionsConnection; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +52,7 @@ public class ClientServerInputMultiplexer implements Closeable { private final DuplexConnection serverConnection; private final DuplexConnection clientConnection; private final DuplexConnection source; - private final ResumeAwareConnection clientServerConnection; + private final ResumePositionsConnection clientServerConnection; public ClientServerInputMultiplexer(DuplexConnection source) { this(source, emptyPluginRegistry); @@ -115,7 +115,7 @@ public ClientServerInputMultiplexer(DuplexConnection source, PluginRegistry plug }); } - public ResumeAwareConnection asClientServerConnection() { + public ResumePositionsConnection asClientServerConnection() { return clientServerConnection; } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java index 3309fc83c..5f92488b1 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java @@ -16,10 +16,14 @@ package io.rsocket.internal; +import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; import io.rsocket.DuplexConnection; import io.rsocket.keepalive.KeepAliveConnection; -import io.rsocket.resume.*; +import io.rsocket.resume.ClientRSocketSession; +import io.rsocket.resume.ResumableFramesStore; +import io.rsocket.resume.ResumeStrategy; import java.time.Duration; import java.util.function.Supplier; import reactor.core.publisher.Mono; @@ -29,7 +33,7 @@ public interface ClientSetup { DuplexConnection wrappedConnection(KeepAliveConnection duplexConnection); /*Provide different resume tokens for SETUP / RESUME cases*/ - ResumeToken resumeToken(); + ByteBuf resumeToken(); class DefaultClientSetup implements ClientSetup { @@ -39,48 +43,59 @@ public DuplexConnection wrappedConnection(KeepAliveConnection connection) { } @Override - public ResumeToken resumeToken() { - return ResumeToken.empty(); + public ByteBuf resumeToken() { + return Unpooled.EMPTY_BUFFER; } } class ResumableClientSetup implements ClientSetup { - private final ResumeToken resumeToken; - private final ClientResumeConfiguration config; + private final ByteBuf resumeToken; private final ByteBufAllocator allocator; - private final Mono newConnection; + private final Mono newConnectionFactory; + private final Duration resumeSessionDuration; + private final Supplier resumeStrategySupplier; + private final ResumableFramesStore resumableFramesStore; + private final Duration resumeStreamTimeout; + private final boolean cleanupStoreOnKeepAlive; public ResumableClientSetup( ByteBufAllocator allocator, - Mono newConnection, - ResumeToken resumeToken, + Mono newConnectionFactory, + ByteBuf resumeToken, ResumableFramesStore resumableFramesStore, Duration resumeSessionDuration, Duration resumeStreamTimeout, - Supplier resumeStrategySupplier) { + Supplier resumeStrategySupplier, + boolean cleanupStoreOnKeepAlive) { this.allocator = allocator; - this.newConnection = newConnection; + this.newConnectionFactory = newConnectionFactory; this.resumeToken = resumeToken; - this.config = - new ClientResumeConfiguration( - resumeSessionDuration, - resumeStrategySupplier, - resumableFramesStore, - resumeStreamTimeout); + this.resumeSessionDuration = resumeSessionDuration; + this.resumeStrategySupplier = resumeStrategySupplier; + this.resumableFramesStore = resumableFramesStore; + this.resumeStreamTimeout = resumeStreamTimeout; + this.cleanupStoreOnKeepAlive = cleanupStoreOnKeepAlive; } @Override public DuplexConnection wrappedConnection(KeepAliveConnection connection) { ClientRSocketSession rSocketSession = - new ClientRSocketSession(allocator, connection, config) - .continueWith(newConnection) - .resumeWith(resumeToken); + new ClientRSocketSession( + connection, + allocator, + resumeSessionDuration, + resumeStrategySupplier, + resumableFramesStore, + resumeStreamTimeout, + cleanupStoreOnKeepAlive) + .continueWith(newConnectionFactory) + .resumeToken(resumeToken); return rSocketSession.resumableConnection(); } @Override - public ResumeToken resumeToken() { + public ByteBuf resumeToken() { return resumeToken; } } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java b/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java index 11cc5d1d3..17e9e53cb 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java @@ -29,6 +29,7 @@ import io.rsocket.util.ConnectionUtils; import java.time.Duration; import java.util.function.Function; +import javax.annotation.Nullable; import reactor.core.publisher.Mono; public interface ServerSetup { @@ -42,7 +43,8 @@ Mono acceptRSocketSetup( Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer); /*get KEEP-ALIVE timings based on start frame: SETUP (directly) /RESUME (lookup by resume token)*/ - Function> keepAliveData(); + @Nullable + KeepAliveData keepAliveData(ByteBuf frame); default void dispose() {} @@ -59,7 +61,7 @@ public Mono acceptRSocketSetup( ClientServerInputMultiplexer multiplexer, Function> then) { - if (!ResumeToken.fromBytes(SetupFrameFlyweight.resumeToken(frame)).isEmpty()) { + if (SetupFrameFlyweight.resumeEnabled(frame)) { return sendError(multiplexer, new UnsupportedSetupException("resume not supported")) .doFinally( signalType -> { @@ -83,17 +85,14 @@ public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexe } @Override - public Function> keepAliveData() { - return frame -> { - if (FrameHeaderFlyweight.frameType(frame) == FrameType.SETUP) { - return Mono.just( - new KeepAliveData( - SetupFrameFlyweight.keepAliveInterval(frame), - SetupFrameFlyweight.keepAliveMaxLifetime(frame))); - } else { - return Mono.never(); - } - }; + public KeepAliveData keepAliveData(ByteBuf frame) { + if (FrameHeaderFlyweight.frameType(frame) == FrameType.SETUP) { + return new KeepAliveData( + SetupFrameFlyweight.keepAliveInterval(frame), + SetupFrameFlyweight.keepAliveMaxLifetime(frame)); + } else { + return null; + } } private Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { @@ -104,19 +103,24 @@ private Mono sendError(ClientServerInputMultiplexer multiplexer, Exception class ResumableServerSetup implements ServerSetup { private final ByteBufAllocator allocator; private final SessionManager sessionManager; - private final ServerResumeConfiguration resumeConfig; + private final Duration resumeSessionDuration; + private final Duration resumeStreamTimeout; + private final Function resumeStoreFactory; + private final boolean cleanupStoreOnKeepAlive; public ResumableServerSetup( ByteBufAllocator allocator, SessionManager sessionManager, Duration resumeSessionDuration, Duration resumeStreamTimeout, - Function resumeStoreFactory) { + Function resumeStoreFactory, + boolean cleanupStoreOnKeepAlive) { this.allocator = allocator; this.sessionManager = sessionManager; - this.resumeConfig = - new ServerResumeConfiguration( - resumeSessionDuration, resumeStreamTimeout, resumeStoreFactory); + this.resumeSessionDuration = resumeSessionDuration; + this.resumeStreamTimeout = resumeStreamTimeout; + this.resumeStoreFactory = resumeStoreFactory; + this.cleanupStoreOnKeepAlive = cleanupStoreOnKeepAlive; } @Override @@ -125,8 +129,8 @@ public Mono acceptRSocketSetup( ClientServerInputMultiplexer multiplexer, Function> then) { - ResumeToken token = ResumeToken.fromBytes(SetupFrameFlyweight.resumeToken(frame)); - if (!token.isEmpty()) { + if (SetupFrameFlyweight.resumeEnabled(frame)) { + ByteBuf resumeToken = SetupFrameFlyweight.resumeToken(frame); KeepAliveData keepAliveData = new KeepAliveData( @@ -137,11 +141,14 @@ public Mono acceptRSocketSetup( sessionManager .save( new ServerRSocketSession( - allocator, multiplexer.asClientServerConnection(), - resumeConfig, + allocator, + resumeSessionDuration, + resumeStreamTimeout, + resumeStoreFactory, + resumeToken, keepAliveData, - token)) + cleanupStoreOnKeepAlive)) .resumableConnection(); return then.apply(new ClientServerInputMultiplexer(resumableConnection)); } else { @@ -151,41 +158,37 @@ public Mono acceptRSocketSetup( @Override public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer) { - return sessionManager - .get(ResumeToken.fromBytes(ResumeFrameFlyweight.token(frame))) - .map( - session -> - session - .continueWith(multiplexer.asClientServerConnection()) - .resumeWith(frame) - .onClose() - .then()) - .orElseGet( - () -> - sendError(multiplexer, new RejectedResumeException("unknown resume token")) - .doFinally( - s -> { - frame.release(); - multiplexer.dispose(); - })); + ServerRSocketSession session = sessionManager.get(ResumeFrameFlyweight.token(frame)); + if (session != null) { + return session + .continueWith(multiplexer.asClientServerConnection()) + .resumeWith(frame) + .onClose() + .then(); + } else { + return sendError(multiplexer, new RejectedResumeException("unknown resume token")) + .doFinally( + s -> { + frame.release(); + multiplexer.dispose(); + }); + } } @Override - public Function> keepAliveData() { - return frame -> { - if (FrameHeaderFlyweight.frameType(frame) == FrameType.SETUP) { - return Mono.just( - new KeepAliveData( - SetupFrameFlyweight.keepAliveInterval(frame), - SetupFrameFlyweight.keepAliveMaxLifetime(frame))); + public KeepAliveData keepAliveData(ByteBuf frame) { + if (FrameHeaderFlyweight.frameType(frame) == FrameType.SETUP) { + return new KeepAliveData( + SetupFrameFlyweight.keepAliveInterval(frame), + SetupFrameFlyweight.keepAliveMaxLifetime(frame)); + } else { + ServerRSocketSession session = sessionManager.get(ResumeFrameFlyweight.token(frame)); + if (session != null) { + return session.keepAliveData(); } else { - return sessionManager - .get(ResumeToken.fromBytes(ResumeFrameFlyweight.token(frame))) - .map(ServerRSocketSession::keepAliveData) - .map(Mono::just) - .orElseGet(Mono::never); + return null; } - }; + } } private Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveConnection.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveConnection.java index 76ad66a60..738b13b02 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveConnection.java @@ -23,33 +23,36 @@ import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; import io.rsocket.internal.KeepAliveData; -import io.rsocket.resume.ResumeAwareConnection; +import io.rsocket.resume.ResumePositionsConnection; import io.rsocket.resume.ResumeStateHolder; import io.rsocket.util.DuplexConnectionProxy; import io.rsocket.util.Function3; import java.time.Duration; -import java.util.Optional; import java.util.function.Consumer; import java.util.function.Function; +import javax.annotation.Nullable; import org.reactivestreams.Publisher; -import reactor.core.publisher.*; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; -public class KeepAliveConnection extends DuplexConnectionProxy implements ResumeAwareConnection { +public class KeepAliveConnection extends DuplexConnectionProxy + implements ResumePositionsConnection { private final MonoProcessor keepAliveHandlerReady = MonoProcessor.create(); private final ByteBufAllocator allocator; - private final Function> keepAliveData; + private final Function keepAliveData; private final Function3 keepAliveHandlerFactory; private final Consumer errorConsumer; - private final UnicastProcessor keepAliveFrames = UnicastProcessor.create(); - private final EmitterProcessor lastReceivedPositions = EmitterProcessor.create(); private volatile KeepAliveHandler keepAliveHandler; + private volatile ResumeStateHolder resumeStateHolder; + private volatile boolean keepAliveHandlerStarted; public static KeepAliveConnection ofClient( ByteBufAllocator allocator, DuplexConnection duplexConnection, - Function> keepAliveData, + Function keepAliveData, Consumer errorConsumer) { return new KeepAliveConnection( @@ -59,7 +62,7 @@ public static KeepAliveConnection ofClient( public static KeepAliveConnection ofServer( ByteBufAllocator allocator, DuplexConnection duplexConnection, - Function> keepAliveData, + Function keepAliveData, Consumer errorConsumer) { return new KeepAliveConnection( @@ -69,7 +72,7 @@ public static KeepAliveConnection ofServer( private KeepAliveConnection( ByteBufAllocator allocator, DuplexConnection duplexConnection, - Function> keepAliveData, + Function keepAliveData, Function3 keepAliveHandlerFactory, Consumer errorConsumer) { super(duplexConnection); @@ -82,7 +85,8 @@ private KeepAliveConnection( private void startKeepAlives(KeepAliveHandler keepAliveHandler) { this.keepAliveHandler = keepAliveHandler; - send(keepAliveFrames).subscribe(null, err -> keepAliveHandler.dispose()); + + send(keepAliveHandler.send()).subscribe(null, err -> keepAliveHandler.dispose()); keepAliveHandler .timeout() @@ -94,20 +98,12 @@ private void startKeepAlives(KeepAliveHandler keepAliveHandler) { errorConsumer.accept(err); dispose(); }); - keepAliveHandler.send().subscribe(keepAliveFrames::onNext); keepAliveHandler.start(); } @Override public Mono send(Publisher frames) { - return super.send( - Flux.from(frames) - .doOnNext( - f -> { - if (isStartFrame(f)) { - keepAliveHandler(keepAliveData.apply(f)).subscribe(keepAliveHandlerReady); - } - })); + return super.send(Flux.from(frames).doOnNext(this::startKeepAliveHandlerOnce)); } @Override @@ -117,11 +113,14 @@ public Flux receive() { f -> { if (isKeepAliveFrame(f)) { long receivedPos = keepAliveHandler.receive(f); - if (isResumeRequested() && receivedPos > 0) { - lastReceivedPositions.onNext(receivedPos); + if (receivedPos > 0) { + ResumeStateHolder h = this.resumeStateHolder; + if (h != null) { + h.onImpliedPosition(receivedPos); + } } - } else if (isStartFrame(f)) { - keepAliveHandler(keepAliveData.apply(f)).subscribe(keepAliveHandlerReady); + } else { + startKeepAliveHandlerOnce(f); } }); } @@ -129,36 +128,42 @@ public Flux receive() { @Override public Mono onClose() { return super.onClose() - .then(Mono.fromRunnable(lastReceivedPositions::onComplete)) - .then( - Mono.fromRunnable( - () -> - Optional.ofNullable(keepAliveHandlerReady.peek()) - .ifPresent(KeepAliveHandler::dispose))); + .doFinally( + s -> { + KeepAliveHandler keepAliveHandler = keepAliveHandlerReady.peek(); + if (keepAliveHandler != null) { + keepAliveHandler.dispose(); + } + }); } @Override - public Flux receiveResumePositions(ResumeStateHolder resumeStateHolder) { - return keepAliveHandlerReady - .doOnNext(h -> h.resumeState(resumeStateHolder)) - .thenMany(lastReceivedPositions); + public void acceptResumeState(ResumeStateHolder resumeStateHolder) { + this.resumeStateHolder = resumeStateHolder; + keepAliveHandlerReady.subscribe(h -> h.resumeState(resumeStateHolder)); } - private boolean isResumeRequested() { - return keepAliveHandler.hasResumeState(); + private void startKeepAliveHandlerOnce(ByteBuf f) { + if (!keepAliveHandlerStarted && isStartFrame(f)) { + keepAliveHandlerStarted = true; + startKeepAliveHandler(keepAliveData.apply(f)); + } } private static boolean isStartFrame(ByteBuf frame) { - return FrameHeaderFlyweight.frameType(frame) == FrameType.SETUP - || FrameHeaderFlyweight.frameType(frame) == FrameType.RESUME; + FrameType frameType = FrameHeaderFlyweight.frameType(frame); + return frameType == FrameType.SETUP || frameType == FrameType.RESUME; } private static boolean isKeepAliveFrame(ByteBuf frame) { return FrameHeaderFlyweight.frameType(frame) == FrameType.KEEPALIVE; } - private Mono keepAliveHandler(Mono keepAliveData) { - return keepAliveData.map( - kad -> keepAliveHandlerFactory.apply(allocator, kad.getTickPeriod(), kad.getTimeout())); + private void startKeepAliveHandler(@Nullable KeepAliveData kad) { + if (kad != null) { + KeepAliveHandler handler = + keepAliveHandlerFactory.apply(allocator, kad.getTickPeriod(), kad.getTimeout()); + keepAliveHandlerReady.onNext(handler); + } } } diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java index 9385ee1c0..7a07d2ea5 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java @@ -22,7 +22,6 @@ import io.rsocket.frame.KeepAliveFrameFlyweight; import io.rsocket.resume.ResumeStateHolder; import java.time.Duration; -import java.util.Optional; import java.util.concurrent.atomic.AtomicReference; import reactor.core.Disposable; import reactor.core.Disposables; @@ -32,10 +31,10 @@ import reactor.core.publisher.UnicastProcessor; abstract class KeepAliveHandler implements Disposable { - protected final ByteBufAllocator allocator; + final ByteBufAllocator allocator; private final Duration keepAlivePeriod; - private final Duration keepAliveTimeout; - private volatile Optional resumeStateHolder = Optional.empty(); + private final long keepAliveTimeout; + private volatile ResumeStateHolder resumeStateHolder; private final UnicastProcessor sent = UnicastProcessor.create(); private final MonoProcessor timeout = MonoProcessor.create(); private final AtomicReference intervalDisposable = new AtomicReference<>(); @@ -55,7 +54,7 @@ private KeepAliveHandler( ByteBufAllocator allocator, Duration keepAlivePeriod, Duration keepAliveTimeout) { this.allocator = allocator; this.keepAlivePeriod = keepAlivePeriod; - this.keepAliveTimeout = keepAliveTimeout; + this.keepAliveTimeout = keepAliveTimeout.toMillis(); } public void start() { @@ -90,11 +89,7 @@ public long receive(ByteBuf keepAliveFrame) { } public void resumeState(ResumeStateHolder resumeStateHolder) { - this.resumeStateHolder = Optional.of(resumeStateHolder); - } - - public boolean hasResumeState() { - return resumeStateHolder.isPresent(); + this.resumeStateHolder = resumeStateHolder; } public Flux send() { @@ -113,13 +108,13 @@ void doSend(ByteBuf frame) { void doCheckTimeout() { long now = System.currentTimeMillis(); - if (now - lastReceivedMillis >= keepAliveTimeout.toMillis()) { - timeout.onNext(new KeepAlive(keepAlivePeriod.toMillis(), keepAliveTimeout.toMillis())); + if (now - lastReceivedMillis >= keepAliveTimeout) { + timeout.onNext(new KeepAlive(keepAlivePeriod.toMillis(), keepAliveTimeout)); } } - Long obtainLastReceivedPos() { - return resumeStateHolder.map(ResumeStateHolder::impliedPosition).orElse(0L); + long obtainLastReceivedPos() { + return resumeStateHolder != null ? resumeStateHolder.impliedPosition() : 0; } private static class Server extends KeepAliveHandler { diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java index c16674744..afe1d4a76 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -24,40 +24,49 @@ import io.rsocket.frame.ResumeFrameFlyweight; import io.rsocket.frame.ResumeOkFrameFlyweight; import io.rsocket.internal.ClientServerInputMultiplexer; -import java.util.Objects; +import java.time.Duration; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; -public class ClientRSocketSession implements RSocketSession> { +public class ClientRSocketSession + implements RSocketSession> { private static final Logger logger = LoggerFactory.getLogger(ClientRSocketSession.class); private final ResumableDuplexConnection resumableConnection; - private volatile Mono newConnection; - private volatile ResumeToken resumeToken; + private volatile Mono newConnection; + private volatile ByteBuf resumeToken; private final ByteBufAllocator allocator; public ClientRSocketSession( + ResumePositionsConnection duplexConnection, ByteBufAllocator allocator, - ResumeAwareConnection duplexConnection, - ClientResumeConfiguration config) { - this.allocator = Objects.requireNonNull(allocator); + Duration resumeSessionDuration, + Supplier resumeStrategy, + ResumableFramesStore resumableFramesStore, + Duration resumeStreamTimeout, + boolean cleanupStoreOnKeepAlive) { + this.allocator = allocator; this.resumableConnection = new ResumableDuplexConnection( "client", duplexConnection, - ResumedFramesCalculator.ofClient, - config.resumeStore(), - config.resumeStreamTimeout()); + resumableFramesStore, + resumeStreamTimeout, + cleanupStoreOnKeepAlive); + + /*session completed: release token initially retained in resumeToken(ByteBuf)*/ + onClose().doFinally(s -> resumeToken.release()).subscribe(); resumableConnection .connectionErrors() .flatMap( err -> { logger.debug("Client session connection error. Starting new connection"); - ResumeStrategy reconnectOnError = config.resumptionStrategy().get(); - ClientResume clientResume = new ClientResume(config.sessionDuration(), resumeToken); + ResumeStrategy reconnectOnError = resumeStrategy.get(); + ClientResume clientResume = new ClientResume(resumeSessionDuration, resumeToken); AtomicBoolean once = new AtomicBoolean(); return newConnection .delaySubscription( @@ -74,25 +83,27 @@ public ClientRSocketSession( retryErr -> Mono.from(reconnectOnError.apply(clientResume, retryErr)) .doOnNext(v -> logger.debug("Retrying with: {}", v)))) - .timeout(config.sessionDuration()); + .timeout(resumeSessionDuration); }) .map(ClientServerInputMultiplexer::new) .subscribe( multiplexer -> { /*reconnect resumable connection*/ reconnect(multiplexer.asClientServerConnection()); - - ResumptionState state = resumableConnection.state(); + long impliedPosition = resumableConnection.impliedPosition(); + long position = resumableConnection.position(); logger.debug( - "Client ResumableConnection reconnected. Sending RESUME frame with state: {}", - state); + "Client ResumableConnection reconnected. Sending RESUME frame with state: [impliedPos: {}, pos: {}]", + impliedPosition, + position); /*Connection is established again: send RESUME frame to server, listen for RESUME_OK*/ sendFrame( ResumeFrameFlyweight.encode( allocator, - resumeToken.toByteArray(), - state.impliedPosition(), - state.position())) + /*retain so token is not released once sent as part of resume frame*/ + resumeToken.retain(), + impliedPosition, + position)) .then(multiplexer.asSetupConnection().receive().next()) .subscribe(this::resumeWith); }, @@ -103,7 +114,8 @@ public ClientRSocketSession( } @Override - public ClientRSocketSession continueWith(Mono newConnection) { + public ClientRSocketSession continueWith( + Mono newConnection) { this.newConnection = newConnection; return this; } @@ -111,9 +123,13 @@ public ClientRSocketSession continueWith(Mono n @Override public ClientRSocketSession resumeWith(ByteBuf resumeOkFrame) { logger.debug("ResumeOK FRAME received"); - ResumptionState resumptionState = stateFromFrame(resumeOkFrame); + long remotePos = remotePos(resumeOkFrame); + long remoteImpliedPos = remoteImpliedPos(resumeOkFrame); + resumeOkFrame.release(); + resumableConnection.resume( - resumptionState, + remotePos, + remoteImpliedPos, pos -> pos.then() /*Resumption is impossible: send CONNECTION_ERROR*/ @@ -121,22 +137,21 @@ public ClientRSocketSession resumeWith(ByteBuf resumeOkFrame) { err -> sendFrame( ErrorFrameFlyweight.encode( - allocator, - 0, - errorFrameThrowable(resumptionState.impliedPosition()))) + allocator, 0, errorFrameThrowable(remoteImpliedPos))) .then(Mono.fromRunnable(resumableConnection::dispose)) /*Resumption is impossible: no need to return control to ResumableConnection*/ .then(Mono.never()))); return this; } - public ClientRSocketSession resumeWith(ResumeToken resumeToken) { - this.resumeToken = resumeToken; + public ClientRSocketSession resumeToken(ByteBuf resumeToken) { + /*retain so token is not released once sent as part of setup frame*/ + this.resumeToken = resumeToken.retain(); return this; } @Override - public void reconnect(ResumeAwareConnection connection) { + public void reconnect(ResumePositionsConnection connection) { resumableConnection.reconnect(connection); } @@ -146,7 +161,7 @@ public DuplexConnection resumableConnection() { } @Override - public ResumeToken token() { + public ByteBuf token() { return resumeToken; } @@ -154,10 +169,12 @@ private Mono sendFrame(ByteBuf frame) { return resumableConnection.sendOne(frame).onErrorResume(err -> Mono.empty()); } - private static ResumptionState stateFromFrame(ByteBuf resumeOkFrame) { - long impliedPos = ResumeOkFrameFlyweight.lastReceivedClientPos(resumeOkFrame); - resumeOkFrame.release(); - return ResumptionState.fromServer(impliedPos); + private static long remoteImpliedPos(ByteBuf resumeOkFrame) { + return ResumeOkFrameFlyweight.lastReceivedClientPos(resumeOkFrame); + } + + private static long remotePos(ByteBuf resumeOkFrame) { + return -1; } private static ConnectionErrorException errorFrameThrowable(long impliedPos) { diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java index f757447d2..a8f9eba05 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java @@ -16,13 +16,14 @@ package io.rsocket.resume; +import io.netty.buffer.ByteBuf; import java.time.Duration; class ClientResume { private final Duration sessionDuration; - private final ResumeToken resumeToken; + private final ByteBuf resumeToken; - public ClientResume(Duration sessionDuration, ResumeToken resumeToken) { + public ClientResume(Duration sessionDuration, ByteBuf resumeToken) { this.sessionDuration = sessionDuration; this.resumeToken = resumeToken; } @@ -31,7 +32,7 @@ public Duration sessionDuration() { return sessionDuration; } - public ResumeToken resumeToken() { + public ByteBuf resumeToken() { return resumeToken; } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java index b241701cd..8a8692cab 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -18,8 +18,8 @@ import io.netty.buffer.ByteBuf; import java.util.Queue; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; @@ -29,68 +29,65 @@ public class InMemoryResumableFramesStore implements ResumableFramesStore { private static final Logger logger = LoggerFactory.getLogger(InMemoryResumableFramesStore.class); + private static final long SAVE_REQUEST_SIZE = Long.MAX_VALUE; private final MonoProcessor disposed = MonoProcessor.create(); - private final AtomicLong position = new AtomicLong(); - private final AtomicLong impliedPosition = new AtomicLong(); - private final AtomicInteger cachedFramesSize = new AtomicInteger(); - private final Queue cachedFrames; + volatile long position; + volatile long impliedPosition; + volatile int cacheSize; + final Queue cachedFrames; private final String tag; private final int cacheLimit; - private final AtomicInteger upstreamFrameRefCnt = new AtomicInteger(); + private volatile int upstreamFrameRefCnt; - public InMemoryResumableFramesStore(String tag, int cacheLimit) { + public InMemoryResumableFramesStore(String tag, int cacheSizeBytes) { this.tag = tag; - this.cacheLimit = cacheLimit; - this.cachedFrames = cachedFramesQueue(cacheLimit); + this.cacheLimit = cacheSizeBytes; + this.cachedFrames = cachedFramesQueue(cacheSizeBytes); } public Mono saveFrames(Flux frames) { - return Mono.defer( - () -> { - MonoProcessor completed = MonoProcessor.create(); - frames - .doFinally(s -> completed.onComplete()) - .subscribe( - frame -> { - cachedFramesSize.incrementAndGet(); - upstreamFrameRefCnt.compareAndSet(0, frame.refCnt()); - cachedFrames.offer(frame.retain()); - if (cachedFramesSize.get() == cacheLimit) { - releaseFrame(); - } - }); - return completed; - }); + MonoProcessor completed = MonoProcessor.create(); + frames + .doFinally(s -> completed.onComplete()) + .subscribe(new FramesSubscriber(SAVE_REQUEST_SIZE)); + return completed; } @Override public void releaseFrames(long remoteImpliedPos) { - long pos = position.get(); + long pos = position; logger.debug( "{} Removing frames for local: {}, remote implied: {}", tag, pos, remoteImpliedPos); - long removeCount = Math.max(0, remoteImpliedPos - pos); - long processedCount = 0; - while (processedCount < removeCount) { - releaseFrame(); - processedCount++; + long removeSize = Math.max(0, remoteImpliedPos - pos); + while (removeSize > 0) { + ByteBuf cachedFrame = cachedFrames.poll(); + if (cachedFrame != null) { + removeSize -= releaseTailFrame(cachedFrame); + } else { + break; + } + } + if (removeSize > 0) { + throw new IllegalStateException( + String.format( + "Local and remote state disagreement: " + + "need to remove additional %d bytes, but cache is empty", + removeSize)); + } else if (removeSize < 0) { + throw new IllegalStateException( + "Local and remote state disagreement: " + "local and remote frame sizes are not equal"); + } else { + logger.debug("{} Removed frames. Current cache size: {}", tag, cacheSize); } - logger.debug("{} Removed frames. Current size: {}", tag, cachedFramesSize.get()); - } - - private void releaseFrame() { - ByteBuf content = cachedFrames.poll(); - cachedFramesSize.decrementAndGet(); - content.release(); - position.incrementAndGet(); } @Override public Flux resumeStream() { return Flux.create( s -> { - int size = cachedFramesSize.get(); - int refCnt = upstreamFrameRefCnt.get(); + int size = cachedFrames.size(); + int refCnt = upstreamFrameRefCnt; logger.debug("{} Resuming stream size: {}", tag, size); /*spsc queue has no iterator - iterating by consuming*/ for (int i = 0; i < size; i++) { @@ -110,17 +107,18 @@ public Flux resumeStream() { @Override public long framePosition() { - return position.get(); + return position; } @Override public long frameImpliedPosition() { - return impliedPosition.get(); + return impliedPosition; } @Override - public void resumableFrameReceived() { - impliedPosition.incrementAndGet(); + public void resumableFrameReceived(ByteBuf frame) { + /*called on transport thread so non-atomic on volatile is safe*/ + impliedPosition += frame.readableBytes(); } @Override @@ -130,7 +128,7 @@ public Mono onClose() { @Override public void dispose() { - cachedFramesSize.set(0); + cacheSize = 0; ByteBuf frame = cachedFrames.poll(); while (frame != null) { frame.release(); @@ -139,7 +137,82 @@ public void dispose() { disposed.onComplete(); } + @Override + public boolean isDisposed() { + return disposed.isTerminated(); + } + + /* this method and saveFrame() won't be called concurrently, + * so non-atomic on volatile is safe*/ + private int releaseTailFrame(ByteBuf content) { + int frameSize = content.readableBytes(); + cacheSize -= frameSize; + position += frameSize; + content.release(); + return frameSize; + } + + /*this method and releaseTailFrame() won't be called concurrently, + * so non-atomic on volatile is safe*/ + void saveFrame(ByteBuf frame) { + if (upstreamFrameRefCnt == 0) { + upstreamFrameRefCnt = frame.refCnt(); + } + + int frameSize = frame.readableBytes(); + long availableSize = cacheLimit - cacheSize; + while (availableSize < frameSize) { + ByteBuf cachedFrame = cachedFrames.poll(); + if (cachedFrame != null) { + availableSize += releaseTailFrame(cachedFrame); + } else { + break; + } + } + if (availableSize >= frameSize) { + cachedFrames.offer(frame.retain()); + cacheSize += frameSize; + } else { + position += frameSize; + } + } + private static Queue cachedFramesQueue(int size) { return Queues.get(size).get(); } + + private class FramesSubscriber implements Subscriber { + private final long firstRequestSize; + private final long refillSize; + private int received; + private Subscription s; + + public FramesSubscriber(long requestSize) { + this.firstRequestSize = requestSize; + this.refillSize = firstRequestSize / 2; + } + + @Override + public void onSubscribe(Subscription s) { + this.s = s; + s.request(firstRequestSize); + } + + @Override + public void onNext(ByteBuf byteBuf) { + saveFrame(byteBuf); + if (firstRequestSize != Long.MAX_VALUE && ++received == refillSize) { + received = 0; + s.request(refillSize); + } + } + + @Override + public void onError(Throwable t) { + logger.info("unexpected onError signal: {}, {}", t.getClass(), t.getMessage()); + } + + @Override + public void onComplete() {} + } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java index fc9261b99..b2a1829a9 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java @@ -23,7 +23,7 @@ public interface RSocketSession extends Closeable { - ResumeToken token(); + ByteBuf token(); DuplexConnection resumableConnection(); @@ -31,7 +31,7 @@ public interface RSocketSession extends Closeable { RSocketSession resumeWith(ByteBuf resumeFrame); - void reconnect(ResumeAwareConnection connection); + void reconnect(ResumePositionsConnection connection); @Override default Mono onClose() { 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 a4bd9eb53..e46a1927f 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -20,10 +20,11 @@ import io.rsocket.Closeable; import io.rsocket.DuplexConnection; import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.internal.UnboundedProcessor; import java.nio.channels.ClosedChannelException; import java.time.Duration; +import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import org.reactivestreams.Publisher; import org.slf4j.Logger; @@ -31,24 +32,25 @@ import reactor.core.Disposable; import reactor.core.Disposables; import reactor.core.publisher.*; +import reactor.util.concurrent.Queues; class ResumableDuplexConnection implements DuplexConnection, ResumeStateHolder { private static final Logger logger = LoggerFactory.getLogger(ResumableDuplexConnection.class); private final String tag; - private final ResumedFramesCalculator resumedFramesCalculator; private final ResumableFramesStore resumableFramesStore; private final Duration resumeStreamTimeout; + private final boolean cleanupOnKeepAlive; private final ReplayProcessor connections = ReplayProcessor.create(1); private final EmitterProcessor connectionErrors = EmitterProcessor.create(); - private volatile ResumeAwareConnection curConnection; + private volatile ResumePositionsConnection curConnection; /*used instead of EmitterProcessor because its autocancel=false capability had no expected effect*/ private final FluxProcessor downStreamFrames = ReplayProcessor.create(0); private final FluxProcessor resumeSaveFrames = EmitterProcessor.create(); private final MonoProcessor resumeSaveCompleted = MonoProcessor.create(); - - private final UnboundedProcessor actions = new UnboundedProcessor<>(); + private final Queue actions = Queues.unboundedMultiproducer().get(); + private final AtomicInteger actionsWip = new AtomicInteger(); private final Mono framesSent; private final RequestListener downStreamRequestListener = new RequestListener(); @@ -56,26 +58,25 @@ class ResumableDuplexConnection implements DuplexConnection, ResumeStateHolder { private final UnicastProcessor> upstreams = UnicastProcessor.create(); private final UpstreamFramesSubscriber upstreamSubscriber = new UpstreamFramesSubscriber( - 128, + Queues.SMALL_BUFFER_SIZE, downStreamRequestListener.requests(), resumeSaveStreamRequestListener.requests(), - actions::onNext); + this::dispatch); - private volatile State state; - private volatile Disposable impliedPosDisposable = Disposables.disposed(); + private volatile int state; private volatile Disposable resumedStreamDisposable = Disposables.disposed(); private final AtomicBoolean disposed = new AtomicBoolean(); ResumableDuplexConnection( String tag, - ResumeAwareConnection duplexConnection, - ResumedFramesCalculator resumedFramesCalculator, + ResumePositionsConnection duplexConnection, ResumableFramesStore resumableFramesStore, - Duration resumeStreamTimeout) { + Duration resumeStreamTimeout, + boolean cleanupOnKeepAlive) { this.tag = tag; - this.resumedFramesCalculator = resumedFramesCalculator; this.resumableFramesStore = resumableFramesStore; this.resumeStreamTimeout = resumeStreamTimeout; + this.cleanupOnKeepAlive = cleanupOnKeepAlive; resumableFramesStore .saveFrames(resumeSaveStreamRequestListener.apply(resumeSaveFrames)) @@ -98,37 +99,32 @@ class ResumableDuplexConnection implements DuplexConnection, ResumeStateHolder { .then() .cache(); - Flux acts = actions.publish().autoConnect(4); - acts.ofType(ByteBuf.class).subscribe(this::sendFrame); - acts.ofType(ResumeStart.class).subscribe(ResumeStart::run); - acts.ofType(Resume.class).subscribe(Resume::run); - acts.ofType(ResumeComplete.class).subscribe(ResumeComplete::run); - reconnect(duplexConnection); } /*reconnected by session after error. After this downstream can receive frames, * but sending in suppressed until resume() is called*/ - public void reconnect(ResumeAwareConnection connection) { + public void reconnect(ResumePositionsConnection connection) { if (curConnection == null) { logger.debug("{} Resumable duplex connection started with connection: {}", tag, connection); state = State.CONNECTED; onNewConnection(connection); + acceptRemoteResumePositions(); } else { logger.debug( "{} Resumable duplex connection reconnected with connection: {}", tag, connection); /*race between sendFrame and doResumeStart may lead to ongoing upstream frames written before resume complete*/ - actions.onNext(new ResumeStart(connection)); + dispatch(new ResumeStart(connection)); } } /*after receiving RESUME (Server) or RESUME_OK (Client) calculate and send resume frames */ public void resume( - ResumptionState peerResumptionState, Function, Mono> resumeFrameSent) { + long remotePos, long remoteImpliedPos, Function, Mono> resumeFrameSent) { /*race between sendFrame and doResume may lead to duplicate frames on resume store*/ - actions.onNext(new Resume(peerResumptionState, resumeFrameSent)); + dispatch(new Resume(remotePos, remoteImpliedPos, resumeFrameSent)); } @Override @@ -138,11 +134,8 @@ public Mono sendOne(ByteBuf frame) { @Override public Mono send(Publisher frames) { - return Mono.defer( - () -> { - upstreams.onNext(Flux.from(frames)); - return framesSent; - }); + upstreams.onNext(Flux.from(frames)); + return framesSent; } @Override @@ -153,17 +146,29 @@ public Flux receive() { .doOnNext( f -> { if (isResumableFrame(f)) { - resumableFramesStore.resumableFrameReceived(); + resumableFramesStore.resumableFrameReceived(f); } }) .onErrorResume(err -> Mono.never())); } + public long position() { + return resumableFramesStore.framePosition(); + } + @Override public long impliedPosition() { return resumableFramesStore.frameImpliedPosition(); } + @Override + public void onImpliedPosition(long remoteImpliedPos) { + logger.debug("Got remote position from keep-alive: {}", remoteImpliedPos); + if (cleanupOnKeepAlive) { + dispatch(new ReleaseFrames(remoteImpliedPos)); + } + } + @Override public Mono onClose() { return Flux.merge(connections.last().flatMap(Closeable::onClose), resumeSaveCompleted).then(); @@ -179,7 +184,6 @@ public void dispose() { resumeSaveFrames.onComplete(); curConnection.dispose(); upstreamSubscriber.dispose(); - impliedPosDisposable.dispose(); resumedStreamDisposable.dispose(); resumableFramesStore.dispose(); } @@ -196,17 +200,12 @@ public boolean isDisposed() { } private void acceptRemoteResumePositions() { - impliedPosDisposable.dispose(); - impliedPosDisposable = - curConnection - .receiveResumePositions(this) - .doOnNext(l -> logger.debug("Got remote position from keep-alive: {}", l)) - .subscribe(this::releaseFramesToPosition); + curConnection.acceptResumeState(this); } private void sendFrame(ByteBuf f) { /*resuming from store so no need to save again*/ - if (isResumableFrame(f) && state != State.RESUME) { + if (state != State.RESUME && isResumableFrame(f)) { resumeSaveFrames.onNext(f); } /*filter frames coming from upstream before actual resumption began, @@ -216,16 +215,25 @@ private void sendFrame(ByteBuf f) { } } - ResumptionState state() { - return new ResumptionState( - resumableFramesStore.framePosition(), resumableFramesStore.frameImpliedPosition()); - } - Flux connectionErrors() { return connectionErrors; } - private void doResumeStart(ResumeAwareConnection connection) { + private void dispatch(Object action) { + actions.offer(action); + if (actionsWip.getAndIncrement() == 0) { + do { + Object a = actions.poll(); + if (a instanceof ByteBuf) { + sendFrame((ByteBuf) a); + } else { + ((Runnable) a).run(); + } + } while (actionsWip.decrementAndGet() != 0); + } + } + + private void doResumeStart(ResumePositionsConnection connection) { state = State.RESUME_STARTED; resumedStreamDisposable.dispose(); upstreamSubscriber.resumeStart(); @@ -233,34 +241,60 @@ private void doResumeStart(ResumeAwareConnection connection) { } private void doResume( - ResumptionState peerResumptionState, Function, Mono> sendResumeFrame) { - ResumptionState localResumptionState = state(); + long remotePosition, + long remoteImpliedPosition, + Function, Mono> sendResumeFrame) { + long localPosition = position(); + long localImpliedPosition = impliedPosition(); + logger.debug("Resumption start"); logger.debug( - "Resumption start. Calculating implied pos using: {}", - resumedFramesCalculator.getClass().getSimpleName()); - logger.debug( - "Resumption states. local: {}, remote: {}", localResumptionState, peerResumptionState); - - Mono res = resumedFramesCalculator.calculate(localResumptionState, peerResumptionState); - Mono localImpliedPos = - res.doOnSuccess(notUsed -> state = State.RESUME) - .doOnSuccess(this::releaseFramesToPosition) - .map(remoteImpliedPos -> localResumptionState.impliedPosition()); + "Resumption states. local: [pos: {}, impliedPos: {}], remote: [pos: {}, impliedPos: {}]", + localPosition, + localImpliedPosition, + remotePosition, + remoteImpliedPosition); + + long remoteImpliedPos = + calculateRemoteImpliedPos( + localPosition, localImpliedPosition, + remotePosition, remoteImpliedPosition); + + Mono impliedPositionOrError; + if (remoteImpliedPos >= 0) { + state = State.RESUME; + releaseFramesToPosition(remoteImpliedPos); + impliedPositionOrError = Mono.just(localImpliedPosition); + } else { + impliedPositionOrError = + Mono.error( + new ResumeStateException( + localPosition, localImpliedPosition, + remotePosition, remoteImpliedPosition)); + } sendResumeFrame - .apply(localImpliedPos) + .apply(impliedPositionOrError) .then( streamResumedFrames( resumableFramesStore .resumeStream() .timeout(resumeStreamTimeout) - .doFinally(s -> actions.onNext(new ResumeComplete()))) + .doFinally(s -> dispatch(new ResumeComplete()))) .doOnError(err -> dispose())) .onErrorResume(err -> Mono.empty()) .subscribe(); } + static long calculateRemoteImpliedPos( + long pos, long impliedPos, long remotePos, long remoteImpliedPos) { + if (remotePos <= impliedPos && pos <= remoteImpliedPos) { + return remoteImpliedPos; + } else { + return -1L; + } + } + private void doResumeComplete() { logger.debug("Completing resumption"); state = State.RESUME_COMPLETED; @@ -273,14 +307,14 @@ private Mono streamResumedFrames(Flux frames) { s -> { ResumeFramesSubscriber subscriber = new ResumeFramesSubscriber( - downStreamRequestListener.requests(), actions::onNext, s::error, s::success); + downStreamRequestListener.requests(), this::dispatch, s::error, s::success); s.onDispose(subscriber); resumedStreamDisposable = subscriber; frames.subscribe(subscriber); }); } - private void onNewConnection(ResumeAwareConnection connection) { + private void onNewConnection(ResumePositionsConnection connection) { curConnection = connection; connection.onClose().doFinally(v -> disconnect(connection)).subscribe(); connections.onNext(connection); @@ -288,7 +322,7 @@ private void onNewConnection(ResumeAwareConnection connection) { private void disconnect(DuplexConnection connection) { /*do not report late disconnects on old connection if new one is available*/ - if (curConnection == connection && state.isActive()) { + if (curConnection == connection && state != State.DISCONNECTED) { Throwable err = new ClosedChannelException(); state = State.DISCONNECTED; logger.debug("{} Inner connection disconnected: {}", tag, err.getClass().getSimpleName()); @@ -298,12 +332,12 @@ private void disconnect(DuplexConnection connection) { /*remove frames confirmed by implied pos, set current pos accordingly*/ - private void releaseFramesToPosition(Long remoteImpliedPos) { + private void releaseFramesToPosition(long remoteImpliedPos) { resumableFramesStore.releaseFrames(remoteImpliedPos); } static boolean isResumableFrame(ByteBuf frame) { - switch (FrameHeaderFlyweight.frameType(frame)) { + switch (FrameHeaderFlyweight.nativeFrameType(frame)) { case REQUEST_CHANNEL: case REQUEST_STREAM: case REQUEST_RESPONSE: @@ -311,36 +345,25 @@ static boolean isResumableFrame(ByteBuf frame) { case REQUEST_N: case CANCEL: case ERROR: - case NEXT: - case NEXT_COMPLETE: + case PAYLOAD: return true; default: return false; } } - private enum State { - CONNECTED(true), - RESUME_STARTED(true), - RESUME(true), - RESUME_COMPLETED(true), - DISCONNECTED(false); - - private final boolean active; - - State(boolean active) { - this.active = active; - } - - public boolean isActive() { - return active; - } + static class State { + static int CONNECTED = 0; + static int RESUME_STARTED = 1; + static int RESUME = 2; + static int RESUME_COMPLETED = 3; + static int DISCONNECTED = 4; } class ResumeStart implements Runnable { - private ResumeAwareConnection connection; + private ResumePositionsConnection connection; - public ResumeStart(ResumeAwareConnection connection) { + public ResumeStart(ResumePositionsConnection connection) { this.connection = connection; } @@ -351,18 +374,20 @@ public void run() { } class Resume implements Runnable { - private final ResumptionState peerResumptionState; + private final long remotePos; + private final long remoteImpliedPos; private final Function, Mono> resumeFrameSent; public Resume( - ResumptionState peerResumptionState, Function, Mono> resumeFrameSent) { - this.peerResumptionState = peerResumptionState; + long remotePos, long remoteImpliedPos, Function, Mono> resumeFrameSent) { + this.remotePos = remotePos; + this.remoteImpliedPos = remoteImpliedPos; this.resumeFrameSent = resumeFrameSent; } @Override public void run() { - doResume(peerResumptionState, resumeFrameSent); + doResume(remotePos, remoteImpliedPos, resumeFrameSent); } } @@ -373,4 +398,17 @@ public void run() { doResumeComplete(); } } + + private class ReleaseFrames implements Runnable { + private final long remoteImpliedPos; + + public ReleaseFrames(long remoteImpliedPos) { + this.remoteImpliedPos = remoteImpliedPos; + } + + @Override + public void run() { + releaseFramesToPosition(remoteImpliedPos); + } + } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java index 849a78dd4..3a30544b6 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableFramesStore.java @@ -51,5 +51,5 @@ public interface ResumableFramesStore extends Closeable { * Received resumable frame as defined by RSocket protocol. Implementation must increment frame * implied position */ - void resumableFrameReceived(); + void resumableFrameReceived(ByteBuf frame); } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeAwareConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumePositionsConnection.java similarity index 79% rename from rsocket-core/src/main/java/io/rsocket/resume/ResumeAwareConnection.java rename to rsocket-core/src/main/java/io/rsocket/resume/ResumePositionsConnection.java index fe6baab4a..07f865131 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumeAwareConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumePositionsConnection.java @@ -17,9 +17,8 @@ package io.rsocket.resume; import io.rsocket.DuplexConnection; -import reactor.core.publisher.Flux; -public interface ResumeAwareConnection extends DuplexConnection { +public interface ResumePositionsConnection extends DuplexConnection { - Flux receiveResumePositions(ResumeStateHolder resumeStateHolder); + void acceptResumeState(ResumeStateHolder resumeStateHolder); } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateException.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateException.java index 173e62bb0..1fae24b07 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateException.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateException.java @@ -18,19 +18,32 @@ class ResumeStateException extends RuntimeException { private static final long serialVersionUID = -5393753463377588732L; - private final ResumptionState local; - private final ResumptionState remote; + private final long localPos; + private final long localImpliedPos; + private final long remotePos; + private final long remoteImpliedPos; - public ResumeStateException(ResumptionState local, ResumptionState remote) { - this.local = local; - this.remote = remote; + public ResumeStateException( + long localPos, long localImpliedPos, long remotePos, long remoteImpliedPos) { + this.localPos = localPos; + this.localImpliedPos = localImpliedPos; + this.remotePos = remotePos; + this.remoteImpliedPos = remoteImpliedPos; } - public ResumptionState localState() { - return local; + public long getLocalPos() { + return localPos; } - public ResumptionState remoteState() { - return remote; + public long getLocalImpliedPos() { + return localImpliedPos; + } + + public long getRemotePos() { + return remotePos; + } + + public long getRemoteImpliedPos() { + return remoteImpliedPos; } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java index 9ab62b002..31687a24b 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStateHolder.java @@ -19,4 +19,6 @@ public interface ResumeStateHolder { long impliedPosition(); + + void onImpliedPosition(long remoteImpliedPos); } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeToken.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeToken.java deleted file mode 100644 index 64d6d51f4..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumeToken.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright 2015-2019 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.resume; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import java.nio.ByteBuffer; -import java.util.Arrays; -import java.util.Objects; -import java.util.UUID; -import javax.annotation.Nullable; - -public final class ResumeToken { - private static final int MAX_TOKEN_LENGTH = 65535; // unsigned 16 bit - private static final ResumeToken EMPTY_TOKEN = new ResumeToken(new byte[0]); - private final byte[] resumeToken; - - protected ResumeToken(byte[] resumeToken) { - assertToken(resumeToken); - this.resumeToken = resumeToken; - } - - public static ResumeToken fromBytes(@Nullable byte[] token) { - return token == null || token.length == 0 ? EMPTY_TOKEN : new ResumeToken(token); - } - - public static ResumeToken empty() { - return EMPTY_TOKEN; - } - - public static ResumeToken generate() { - return new ResumeToken(getBytesFromUUID(UUID.randomUUID())); - } - - static byte[] getBytesFromUUID(UUID uuid) { - ByteBuffer bb = ByteBuffer.wrap(new byte[16]); - bb.putLong(uuid.getMostSignificantBits()); - bb.putLong(uuid.getLeastSignificantBits()); - - return bb.array(); - } - - @Override - public int hashCode() { - return Arrays.hashCode(resumeToken); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof ResumeToken) { - return Arrays.equals(resumeToken, ((ResumeToken) obj).resumeToken); - } - return false; - } - - @Override - public String toString() { - return ByteBufUtil.hexDump(resumeToken); - } - - public int length() { - return resumeToken.length; - } - - public boolean isEmpty() { - return length() == 0; - } - - public byte[] toByteArray() { - return resumeToken; - } - - public ByteBuf toByteBuf() { - return isEmpty() ? Unpooled.EMPTY_BUFFER : Unpooled.wrappedBuffer(toByteArray()); - } - - private static void assertToken(byte[] resumeToken) { - Objects.requireNonNull(resumeToken); - int tokenLength = resumeToken.length; - if (tokenLength > MAX_TOKEN_LENGTH) { - throw new IllegalArgumentException( - String.format( - "Resumption token length exceeds limit of %d: %d", MAX_TOKEN_LENGTH, tokenLength)); - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumedFramesCalculator.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumedFramesCalculator.java deleted file mode 100644 index 304470adf..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumedFramesCalculator.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2015-2019 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.resume; - -import reactor.core.publisher.Mono; - -interface ResumedFramesCalculator { - - ResumedFramesCalculator ofClient = new ClientResumedFramesCalculator(); - ResumedFramesCalculator ofServer = new ServerResumedFramesCalculator(); - - Mono calculate(ResumptionState local, ResumptionState remote); - - class ClientResumedFramesCalculator implements ResumedFramesCalculator { - - @Override - public Mono calculate(ResumptionState clientState, ResumptionState serverState) { - long serverImplied = serverState.impliedPosition(); - if (serverImplied >= clientState.position()) { - return Mono.just(serverImplied); - } else { - return Mono.error(new ResumeStateException(clientState, serverState)); - } - } - } - - class ServerResumedFramesCalculator implements ResumedFramesCalculator { - - @Override - public Mono calculate(ResumptionState serverState, ResumptionState clientState) { - boolean clientStateValid = clientState.position() <= serverState.impliedPosition(); - boolean serverStateValid = serverState.position() <= clientState.impliedPosition(); - return clientStateValid && serverStateValid - ? Mono.just(clientState.impliedPosition()) - : Mono.error(new ResumeStateException(serverState, clientState)); - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java index 982513aa1..d2a42e99b 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -24,42 +24,46 @@ import io.rsocket.frame.ResumeFrameFlyweight; import io.rsocket.frame.ResumeOkFrameFlyweight; import io.rsocket.internal.KeepAliveData; -import java.util.Objects; +import java.time.Duration; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.FluxProcessor; import reactor.core.publisher.Mono; import reactor.core.publisher.ReplayProcessor; -public class ServerRSocketSession implements RSocketSession { +public class ServerRSocketSession implements RSocketSession { private static final Logger logger = LoggerFactory.getLogger(ServerRSocketSession.class); private final ResumableDuplexConnection resumableConnection; /*used instead of EmitterProcessor because its autocancel=false capability had no expected effect*/ - private final FluxProcessor newConnections = + private final FluxProcessor newConnections = ReplayProcessor.create(0); private final ByteBufAllocator allocator; private final KeepAliveData keepAliveData; - private final ResumeToken resumeToken; + private final ByteBuf resumeToken; public ServerRSocketSession( + ResumePositionsConnection duplexConnection, ByteBufAllocator allocator, - ResumeAwareConnection duplexConnection, - ServerResumeConfiguration config, + Duration resumeSessionDuration, + Duration resumeStreamTimeout, + Function resumeStoreFactory, + ByteBuf resumeToken, KeepAliveData keepAliveData, - ResumeToken resumeToken) { - this.allocator = Objects.requireNonNull(allocator); - this.keepAliveData = Objects.requireNonNull(keepAliveData); - this.resumeToken = Objects.requireNonNull(resumeToken); + boolean cleanupStoreOnKeepAlive) { + this.allocator = allocator; + this.keepAliveData = keepAliveData; + this.resumeToken = resumeToken; this.resumableConnection = new ResumableDuplexConnection( "server", duplexConnection, - ResumedFramesCalculator.ofServer, - config.resumeStoreFactory().apply(resumeToken), - config.resumeStreamTimeout()); + resumeStoreFactory.apply(resumeToken), + resumeStreamTimeout, + cleanupStoreOnKeepAlive); - Mono timeout = + Mono timeout = resumableConnection .connectionErrors() .flatMap( @@ -68,10 +72,10 @@ public ServerRSocketSession( return newConnections .next() .doOnNext(c -> logger.debug("Connection after error: {}", c)) - .timeout(config.sessionDuration()); + .timeout(resumeSessionDuration); }) .then() - .cast(ResumeAwareConnection.class); + .cast(ResumePositionsConnection.class); newConnections .mergeWith(timeout) @@ -87,7 +91,7 @@ public ServerRSocketSession( } @Override - public ServerRSocketSession continueWith(ResumeAwareConnection newConnection) { + public ServerRSocketSession continueWith(ResumePositionsConnection newConnection) { logger.debug("Server continued with connection: {}", newConnection); newConnections.onNext(newConnection); return this; @@ -96,8 +100,13 @@ public ServerRSocketSession continueWith(ResumeAwareConnection newConnection) { @Override public ServerRSocketSession resumeWith(ByteBuf resumeFrame) { logger.debug("Resume FRAME received"); + long remotePos = remotePos(resumeFrame); + long remoteImpliedPos = remoteImpliedPos(resumeFrame); + resumeFrame.release(); + resumableConnection.resume( - stateFromFrame(resumeFrame), + remotePos, + remoteImpliedPos, pos -> pos.flatMap( impliedPos -> sendFrame(ResumeOkFrameFlyweight.encode(allocator, impliedPos))) @@ -112,7 +121,7 @@ public ServerRSocketSession resumeWith(ByteBuf resumeFrame) { } @Override - public void reconnect(ResumeAwareConnection connection) { + public void reconnect(ResumePositionsConnection connection) { resumableConnection.reconnect(connection); } @@ -122,7 +131,7 @@ public DuplexConnection resumableConnection() { } @Override - public ResumeToken token() { + public ByteBuf token() { return resumeToken; } @@ -135,11 +144,12 @@ private Mono sendFrame(ByteBuf frame) { return resumableConnection.sendOne(frame).onErrorResume(e -> Mono.empty()); } - private static ResumptionState stateFromFrame(ByteBuf resumeFrame) { - long peerPos = ResumeFrameFlyweight.firstAvailableClientPos(resumeFrame); - long peerImpliedPos = ResumeFrameFlyweight.lastReceivedServerPos(resumeFrame); - resumeFrame.release(); - return ResumptionState.fromClient(peerPos, peerImpliedPos); + private static long remotePos(ByteBuf resumeFrame) { + return ResumeFrameFlyweight.firstAvailableClientPos(resumeFrame); + } + + private static long remoteImpliedPos(ByteBuf resumeFrame) { + return ResumeFrameFlyweight.lastReceivedServerPos(resumeFrame); } private static RejectedResumeException errorFrameThrowable(Throwable err) { @@ -148,8 +158,11 @@ private static RejectedResumeException errorFrameThrowable(Throwable err) { ResumeStateException resumeException = ((ResumeStateException) err); msg = String.format( - "resumption_pos=[ remote: %s, local: %s]", - resumeException.remoteState(), resumeException.localState()); + "resumption_pos=[ remote: { pos: %d, impliedPos: %d }, local: { pos: %d, impliedPos: %d }]", + resumeException.getRemotePos(), + resumeException.getRemoteImpliedPos(), + resumeException.getLocalPos(), + resumeException.getLocalImpliedPos()); } else { msg = String.format("resume_internal_error: %s", err.getMessage()); } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerResumeConfiguration.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerResumeConfiguration.java index db291309f..067db2bb8 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerResumeConfiguration.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerResumeConfiguration.java @@ -16,18 +16,19 @@ package io.rsocket.resume; +import io.netty.buffer.ByteBuf; import java.time.Duration; import java.util.function.Function; public class ServerResumeConfiguration { private final Duration sessionDuration; private final Duration resumeStreamTimeout; - private final Function resumeStoreFactory; + private final Function resumeStoreFactory; public ServerResumeConfiguration( Duration sessionDuration, Duration resumeStreamTimeout, - Function resumeStoreFactory) { + Function resumeStoreFactory) { this.sessionDuration = sessionDuration; this.resumeStreamTimeout = resumeStreamTimeout; this.resumeStoreFactory = resumeStoreFactory; @@ -41,7 +42,7 @@ public Duration resumeStreamTimeout() { return resumeStreamTimeout; } - public Function resumeStoreFactory() { + public Function resumeStoreFactory() { return resumeStoreFactory; } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java b/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java index c8c179dcf..3882103a0 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java @@ -16,35 +16,46 @@ package io.rsocket.resume; +import io.netty.buffer.ByteBuf; import java.util.Map; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import javax.annotation.Nullable; public class SessionManager { - private boolean isDisposed; - private final Map sessions = new ConcurrentHashMap<>(); + private volatile boolean isDisposed; + private final Map sessions = new ConcurrentHashMap<>(); public ServerRSocketSession save(ServerRSocketSession session) { if (isDisposed) { session.dispose(); } else { - ResumeToken token = session.token(); - session.onClose().doOnSuccess(v -> sessions.remove(token)).subscribe(); - ServerRSocketSession prev = sessions.put(token, session); - if (prev != null) { - prev.dispose(); + ByteBuf token = session.token().retain(); + session + .onClose() + .doOnSuccess( + v -> { + if (isDisposed || sessions.get(token) == session) { + sessions.remove(token); + } + token.release(); + }) + .subscribe(); + ServerRSocketSession prevSession = sessions.remove(token); + if (prevSession != null) { + prevSession.dispose(); } + sessions.put(token, session); } return session; } - public Optional get(ResumeToken resumeToken) { - return Optional.ofNullable(sessions.get(resumeToken)); + @Nullable + public ServerRSocketSession get(ByteBuf resumeToken) { + return sessions.get(resumeToken); } public void dispose() { isDisposed = true; sessions.values().forEach(ServerRSocketSession::dispose); - sessions.clear(); } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java b/rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java index 377ae9761..f010a05bd 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/UpstreamFramesSubscriber.java @@ -17,7 +17,6 @@ package io.rsocket.resume; import io.netty.buffer.ByteBuf; -import io.rsocket.internal.UnboundedProcessor; import java.util.Queue; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; @@ -33,7 +32,6 @@ class UpstreamFramesSubscriber implements Subscriber, Disposable { private static final Logger logger = LoggerFactory.getLogger(UpstreamFramesSubscriber.class); - private final UnboundedProcessor actions = new UnboundedProcessor<>(); private final AtomicBoolean disposed = new AtomicBoolean(); private final Consumer itemConsumer; private final Disposable downstreamRequestDisposable; @@ -58,11 +56,6 @@ class UpstreamFramesSubscriber implements Subscriber, Disposable { resumeSaveStreamDisposable = resumeSaveStreamRequests.subscribe(requestN -> requestN(requestN, 0)); - - Flux acts = actions.publish().autoConnect(3); - acts.ofType(ByteBuf.class).subscribe(this::processFrame); - acts.ofType(ResumeStart.class).subscribe(ResumeStart::run); - acts.ofType(ResumeComplete.class).subscribe(ResumeComplete::run); } @Override @@ -77,7 +70,7 @@ public void onSubscribe(Subscription s) { @Override public void onNext(ByteBuf item) { - actions.onNext(item); + processFrame(item); } @Override @@ -91,11 +84,17 @@ public void onComplete() { } public void resumeStart() { - actions.onNext(new ResumeStart()); + resumeStarted = true; } public void resumeComplete() { - actions.onNext(new ResumeComplete()); + ByteBuf frame = framesCache.poll(); + while (frame != null) { + itemConsumer.accept(frame); + frame = framesCache.poll(); + } + resumeStarted = false; + doRequest(); } @Override @@ -150,20 +149,6 @@ private void releaseCache() { } } - private void doResumeStart() { - resumeStarted = true; - } - - private void doResumeComplete() { - ByteBuf frame = framesCache.poll(); - while (frame != null) { - itemConsumer.accept(frame); - frame = framesCache.poll(); - } - resumeStarted = false; - doRequest(); - } - private void processFrame(ByteBuf item) { if (resumeStarted) { framesCache.offer(item); @@ -171,20 +156,4 @@ private void processFrame(ByteBuf item) { itemConsumer.accept(item); } } - - private class ResumeStart implements Runnable { - - @Override - public void run() { - doResumeStart(); - } - } - - private class ResumeComplete implements Runnable { - - @Override - public void run() { - doResumeComplete(); - } - } } diff --git a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java index eb95464b4..f8708e602 100644 --- a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java @@ -42,14 +42,13 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; -import reactor.core.publisher.ReplayProcessor; public class KeepAliveTest { private static final int TICK_PERIOD = 100; private static final int TIMEOUT = 700; private TestDuplexConnection testConnection; - private Function> timingsProvider; + private Function timingsProvider; private List errors; private Consumer errorConsumer; private KeepAliveConnection clientConnection; @@ -61,7 +60,7 @@ void setUp() { allocator = ByteBufAllocator.DEFAULT; testConnection = new TestDuplexConnection(); - timingsProvider = f -> Mono.just(new KeepAliveData(TICK_PERIOD, TIMEOUT)); + timingsProvider = f -> new KeepAliveData(TICK_PERIOD, TIMEOUT); errors = new ArrayList<>(); errorConsumer = errors::add; @@ -122,8 +121,8 @@ void clientCloseOnMissingKeepalives() { @Test void clientResumptionState() { - ReplayProcessor resumePositions = ReplayProcessor.create(); - clientConnection.receiveResumePositions(new TestResumeStateHolder()).subscribe(resumePositions); + TestResumeStateHolder resumeStateHolder = new TestResumeStateHolder(); + clientConnection.acceptResumeState(resumeStateHolder); clientConnection.sendOne(setupFrame()).subscribe(); clientConnection.receive().subscribe(); @@ -134,8 +133,7 @@ void clientResumptionState() { Mono.delay(Duration.ofMillis(500)).block(); - List receivedPositions = - resumePositions.take(3).timeout(Duration.ofMillis(100)).collectList().block(); + List receivedPositions = resumeStateHolder.receivedImpliedPositions(); Collection sent = testConnection.getSent(); List sentPositions = @@ -215,14 +213,24 @@ private static FrameType frameType(ByteBuf frame) { } private static class TestResumeStateHolder implements ResumeStateHolder { - private List positions = Arrays.asList(1, 5, 6, 8); + private final List sentPositions = Arrays.asList(1L, 5L, 6L, 8L); + private final List receivedPositions = new ArrayList<>(); private int counter = 0; @Override public long impliedPosition() { - Integer res = positions.get(counter); - counter = Math.min(counter + 1, positions.size() - 1); + long res = sentPositions.get(counter); + counter = Math.min(counter + 1, sentPositions.size() - 1); return res; } + + @Override + public void onImpliedPosition(long remoteImpliedPos) { + receivedPositions.add(remoteImpliedPos); + } + + public List receivedImpliedPositions() { + return receivedPositions; + } } } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameFlyweightTest.java index cf4a7063f..f8b481f05 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameFlyweightTest.java @@ -18,6 +18,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; import java.util.Arrays; import org.junit.Assert; import org.junit.jupiter.api.Test; @@ -28,10 +29,11 @@ public class ResumeFrameFlyweightTest { void testEncoding() { byte[] tokenBytes = new byte[65000]; Arrays.fill(tokenBytes, (byte) 1); - ByteBuf byteBuf = ResumeFrameFlyweight.encode(ByteBufAllocator.DEFAULT, tokenBytes, 21, 12); + ByteBuf token = Unpooled.wrappedBuffer(tokenBytes); + ByteBuf byteBuf = ResumeFrameFlyweight.encode(ByteBufAllocator.DEFAULT, token, 21, 12); Assert.assertEquals( ResumeFrameFlyweight.CURRENT_VERSION, ResumeFrameFlyweight.version(byteBuf)); - Assert.assertArrayEquals(tokenBytes, ResumeFrameFlyweight.token(byteBuf)); + Assert.assertEquals(token, ResumeFrameFlyweight.token(byteBuf)); Assert.assertEquals(21, ResumeFrameFlyweight.lastReceivedServerPos(byteBuf)); Assert.assertEquals(12, ResumeFrameFlyweight.firstAvailableClientPos(byteBuf)); byteBuf.release(); diff --git a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java index bbdad87b5..73527536f 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java @@ -34,13 +34,14 @@ void testEncodingResume() { Arrays.fill(tokenBytes, (byte) 1); ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); + ByteBuf token = Unpooled.wrappedBuffer(tokenBytes); ByteBuf frame = SetupFrameFlyweight.encode( ByteBufAllocator.DEFAULT, true, 5, 500, - Unpooled.wrappedBuffer(tokenBytes), + token, "metadata_type", "data_type", metadata, @@ -49,7 +50,7 @@ void testEncodingResume() { assertEquals(FrameType.SETUP, FrameHeaderFlyweight.frameType(frame)); assertTrue(SetupFrameFlyweight.honorLease(frame)); assertTrue(SetupFrameFlyweight.resumeEnabled(frame)); - assertArrayEquals(tokenBytes, SetupFrameFlyweight.resumeToken(frame)); + assertEquals(token, SetupFrameFlyweight.resumeToken(frame)); assertEquals("metadata_type", SetupFrameFlyweight.metadataMimeType(frame)); assertEquals("data_type", SetupFrameFlyweight.dataMimeType(frame)); assertEquals(metadata, SetupFrameFlyweight.metadata(frame)); diff --git a/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java b/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java new file mode 100644 index 000000000..9da66d424 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/resume/InMemoryResumeStoreTest.java @@ -0,0 +1,93 @@ +package io.rsocket.resume; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import java.util.Arrays; +import org.junit.Assert; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +public class InMemoryResumeStoreTest { + + @Test + void saveWithoutTailRemoval() { + InMemoryResumableFramesStore store = inMemoryStore(25); + ByteBuf frame = frameMock(10); + store.saveFrames(Flux.just(frame)).block(); + Assert.assertEquals(1, store.cachedFrames.size()); + Assert.assertEquals(frame.readableBytes(), store.cacheSize); + Assert.assertEquals(0, store.position); + } + + @Test + void saveRemoveOneFromTail() { + InMemoryResumableFramesStore store = inMemoryStore(25); + ByteBuf frame1 = frameMock(20); + ByteBuf frame2 = frameMock(10); + store.saveFrames(Flux.just(frame1, frame2)).block(); + Assert.assertEquals(1, store.cachedFrames.size()); + Assert.assertEquals(frame2.readableBytes(), store.cacheSize); + Assert.assertEquals(frame1.readableBytes(), store.position); + } + + @Test + void saveRemoveTwoFromTail() { + InMemoryResumableFramesStore store = inMemoryStore(25); + ByteBuf frame1 = frameMock(10); + ByteBuf frame2 = frameMock(10); + ByteBuf frame3 = frameMock(20); + store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); + Assert.assertEquals(1, store.cachedFrames.size()); + Assert.assertEquals(frame3.readableBytes(), store.cacheSize); + Assert.assertEquals(size(frame1, frame2), store.position); + } + + @Test + void saveBiggerThanStore() { + InMemoryResumableFramesStore store = inMemoryStore(25); + ByteBuf frame1 = frameMock(10); + ByteBuf frame2 = frameMock(10); + ByteBuf frame3 = frameMock(30); + store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); + Assert.assertEquals(0, store.cachedFrames.size()); + Assert.assertEquals(0, store.cacheSize); + Assert.assertEquals(size(frame1, frame2, frame3), store.position); + } + + @Test + void releaseFrames() { + InMemoryResumableFramesStore store = inMemoryStore(100); + ByteBuf frame1 = frameMock(10); + ByteBuf frame2 = frameMock(10); + ByteBuf frame3 = frameMock(30); + store.saveFrames(Flux.just(frame1, frame2, frame3)).block(); + store.releaseFrames(20); + Assert.assertEquals(1, store.cachedFrames.size()); + Assert.assertEquals(frame3.readableBytes(), store.cacheSize); + Assert.assertEquals(size(frame1, frame2), store.position); + } + + @Test + void receiveImpliedPosition() { + InMemoryResumableFramesStore store = inMemoryStore(100); + ByteBuf frame1 = frameMock(10); + ByteBuf frame2 = frameMock(30); + store.resumableFrameReceived(frame1); + store.resumableFrameReceived(frame2); + Assert.assertEquals(size(frame1, frame2), store.frameImpliedPosition()); + } + + private int size(ByteBuf... byteBufs) { + return Arrays.stream(byteBufs).mapToInt(ByteBuf::readableBytes).sum(); + } + + private static InMemoryResumableFramesStore inMemoryStore(int size) { + return new InMemoryResumableFramesStore("test", size); + } + + private static ByteBuf frameMock(int size) { + byte[] bytes = new byte[size]; + Arrays.fill(bytes, (byte) 7); + return Unpooled.wrappedBuffer(bytes); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java b/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java index a9d6d235f..7d2a7bcc8 100644 --- a/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/resume/ResumeCalculatorTest.java @@ -16,66 +16,42 @@ package io.rsocket.resume; -import java.time.Duration; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import reactor.test.StepVerifier; public class ResumeCalculatorTest { - private ResumedFramesCalculator clientResumeCalculator; - private ResumedFramesCalculator serverResumeCalculator; - @BeforeEach - void setUp() { - clientResumeCalculator = ResumedFramesCalculator.ofClient; - serverResumeCalculator = ResumedFramesCalculator.ofServer; - } + void setUp() {} @Test void clientResumeSuccess() { - ResumptionState local = ResumptionState.fromClient(1, 42); - ResumptionState remote = ResumptionState.fromServer(3); - StepVerifier.create(clientResumeCalculator.calculate(local, remote)) - .expectNext(3L) - .expectComplete() - .verify(Duration.ofSeconds(1)); + long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 42, -1, 3); + Assertions.assertEquals(3, position); } @Test void clientResumeError() { - ResumptionState local = ResumptionState.fromClient(4, 42); - ResumptionState remote = ResumptionState.fromServer(3); - StepVerifier.create(clientResumeCalculator.calculate(local, remote)) - .expectError(ResumeStateException.class) - .verify(Duration.ofSeconds(1)); + long position = ResumableDuplexConnection.calculateRemoteImpliedPos(4, 42, -1, 3); + Assertions.assertEquals(-1, position); } @Test void serverResumeSuccess() { - ResumptionState local = ResumptionState.fromClient(1, 42); - ResumptionState remote = ResumptionState.fromClient(4, 23); - StepVerifier.create(serverResumeCalculator.calculate(local, remote)) - .expectNext(23L) - .expectComplete() - .verify(Duration.ofSeconds(1)); + long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 42, 4, 23); + Assertions.assertEquals(23, position); } @Test void serverResumeErrorClientState() { - ResumptionState local = ResumptionState.fromClient(1, 3); - ResumptionState remote = ResumptionState.fromClient(4, 23); - StepVerifier.create(serverResumeCalculator.calculate(local, remote)) - .expectError(ResumeStateException.class) - .verify(Duration.ofSeconds(1)); + long position = ResumableDuplexConnection.calculateRemoteImpliedPos(1, 3, 4, 23); + Assertions.assertEquals(-1, position); } @Test void serverResumeErrorServerState() { - ResumptionState local = ResumptionState.fromClient(4, 42); - ResumptionState remote = ResumptionState.fromClient(4, 1); - StepVerifier.create(serverResumeCalculator.calculate(local, remote)) - .expectError(ResumeStateException.class) - .verify(Duration.ofSeconds(1)); + long position = ResumableDuplexConnection.calculateRemoteImpliedPos(4, 42, 4, 1); + Assertions.assertEquals(-1, position); } } diff --git a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java index f4064e63b..009d0d8db 100644 --- a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java @@ -211,7 +211,9 @@ private static Mono newClientRSocket( return RSocketFactory.connect() .resume() .resumeSessionDuration(Duration.ofSeconds(sessionDurationSeconds)) - .keepAliveTickPeriod(Duration.ofSeconds(30)) + .resumeStore(t -> new InMemoryResumableFramesStore("client", 500_000)) + .resumeCleanupOnKeepAlive() + .keepAliveTickPeriod(Duration.ofSeconds(5)) .keepAliveAckTimeout(Duration.ofMinutes(5)) .errorConsumer(errConsumer) .resumeStrategy(() -> new PeriodicResumeStrategy(Duration.ofSeconds(1))) @@ -226,8 +228,9 @@ private static Mono newServerRSocket() { private static Mono newServerRSocket(int sessionDurationSeconds) { return RSocketFactory.receive() .resume() - .resumeStore(t -> new InMemoryResumableFramesStore("server", 100_000)) + .resumeStore(t -> new InMemoryResumableFramesStore("server", 500_000)) .resumeSessionDuration(Duration.ofSeconds(sessionDurationSeconds)) + .resumeCleanupOnKeepAlive() .acceptor((setupPayload, rSocket) -> Mono.just(new TestResponderRSocket())) .transport(serverTransport(SERVER_HOST, SERVER_PORT)) .start(); From 837a038706398551b2b0d3ea3994effd18ede06a Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 8 Apr 2019 14:07:32 +0300 Subject: [PATCH 021/181] prepares for release --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4b4f4bd35..45f2598a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,4 +12,4 @@ # limitations under the License. # -version=0.12.1-RC3-SNAPSHOT +version=0.12.1-RC3 From 7ca471f0e5edc08256c5470d894f745ff109ea8f Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 8 Apr 2019 14:09:24 +0300 Subject: [PATCH 022/181] bumps to the next iteration --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 45f2598a1..6a4960955 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,4 +12,4 @@ # limitations under the License. # -version=0.12.1-RC3 +version=0.12.1-RC4-SNAPSHOT From de7ee885cc681378089c272f55544aaca2b36725 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Fri, 12 Apr 2019 11:37:43 -0700 Subject: [PATCH 023/181] fixes issue with fragmentation when the MTU size is set to 64 bytes (#616) Signed-off-by: Robert Roeser --- .../FragmentationDuplexConnection.java | 27 +++++++++++++++++-- .../fragmentation/FrameReassembler.java | 26 +++++++++--------- .../frame/decoder/DefaultPayloadDecoder.java | 2 +- .../FragmentationDuplexConnectionTest.java | 22 ++++++++------- .../transport/local/LocalClientTransport.java | 2 +- .../transport/local/LocalServerTransport.java | 2 +- .../netty/client/TcpClientTransport.java | 6 ++++- .../client/WebsocketClientTransport.java | 2 +- .../netty/server/TcpServerTransport.java | 6 ++++- .../netty/server/WebsocketRouteTransport.java | 3 ++- .../server/WebsocketServerTransport.java | 2 +- .../io/rsocket/integration/FragmentTest.java | 24 ++++++++++------- .../src/test/resources/logback-test.xml | 1 + 13 files changed, 83 insertions(+), 42 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java index 023c4e689..68e15fe70 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java @@ -20,6 +20,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; import io.rsocket.DuplexConnection; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameLengthFlyweight; @@ -46,9 +47,14 @@ public final class FragmentationDuplexConnection implements DuplexConnection { private final ByteBufAllocator allocator; private final FrameReassembler frameReassembler; private final boolean encodeLength; + private final String type; public FragmentationDuplexConnection( - DuplexConnection delegate, ByteBufAllocator allocator, int mtu, boolean encodeLength) { + DuplexConnection delegate, + ByteBufAllocator allocator, + int mtu, + boolean encodeLength, + String type) { Objects.requireNonNull(delegate, "delegate must not be null"); Objects.requireNonNull(allocator, "byteBufAllocator must not be null"); if (mtu < MIN_MTU_SIZE) { @@ -59,6 +65,7 @@ public FragmentationDuplexConnection( this.delegate = delegate; this.mtu = mtu; this.frameReassembler = new FrameReassembler(allocator); + this.type = type; delegate.onClose().doFinally(s -> frameReassembler.dispose()).subscribe(); } @@ -77,7 +84,23 @@ public Mono sendOne(ByteBuf frame) { FrameType frameType = FrameHeaderFlyweight.frameType(frame); int readableBytes = frame.readableBytes(); if (shouldFragment(frameType, readableBytes)) { - return delegate.send(fragmentFrame(allocator, mtu, frame, frameType, encodeLength)); + if (logger.isDebugEnabled()) { + return delegate.send( + Flux.from(fragmentFrame(allocator, mtu, frame, frameType, encodeLength)) + .doOnNext( + byteBuf -> { + ByteBuf frame1 = FrameLengthFlyweight.frame(byteBuf); + logger.debug( + "{} - stream id {} - frame type {} - \n {}", + type, + FrameHeaderFlyweight.streamId(frame1), + FrameHeaderFlyweight.frameType(frame1), + ByteBufUtil.prettyHexDump(frame1)); + })); + } else { + return delegate.send( + Flux.from(fragmentFrame(allocator, mtu, frame, frameType, encodeLength))); + } } else { return delegate.sendOne(encode(frame)); } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java index 32815779a..4f65ebf63 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java @@ -172,23 +172,23 @@ void handleFollowsFlag(ByteBuf frame, int streamId, FrameType frameType) { CompositeByteBuf metadata = getMetadata(streamId); switch (frameType) { case REQUEST_FNF: - metadata.addComponents(true, RequestFireAndForgetFrameFlyweight.metadata(frame)); + metadata.addComponents(true, RequestFireAndForgetFrameFlyweight.metadata(frame).retain()); break; case REQUEST_STREAM: - metadata.addComponents(true, RequestStreamFrameFlyweight.metadata(frame)); + metadata.addComponents(true, RequestStreamFrameFlyweight.metadata(frame).retain()); break; case REQUEST_RESPONSE: - metadata.addComponents(true, RequestResponseFrameFlyweight.metadata(frame)); + metadata.addComponents(true, RequestResponseFrameFlyweight.metadata(frame).retain()); break; case REQUEST_CHANNEL: - metadata.addComponents(true, RequestChannelFrameFlyweight.metadata(frame)); + metadata.addComponents(true, RequestChannelFrameFlyweight.metadata(frame).retain()); break; // Payload and synthetic types case PAYLOAD: case NEXT: case NEXT_COMPLETE: case COMPLETE: - metadata.addComponents(true, PayloadFrameFlyweight.metadata(frame)); + metadata.addComponents(true, PayloadFrameFlyweight.metadata(frame).retain()); break; default: throw new IllegalStateException("unsupported fragment type"); @@ -198,23 +198,23 @@ void handleFollowsFlag(ByteBuf frame, int streamId, FrameType frameType) { ByteBuf data; switch (frameType) { case REQUEST_FNF: - data = RequestFireAndForgetFrameFlyweight.data(frame); + data = RequestFireAndForgetFrameFlyweight.data(frame).retain(); break; case REQUEST_STREAM: - data = RequestStreamFrameFlyweight.data(frame); + data = RequestStreamFrameFlyweight.data(frame).retain(); break; case REQUEST_RESPONSE: - data = RequestResponseFrameFlyweight.data(frame); + data = RequestResponseFrameFlyweight.data(frame).retain(); break; case REQUEST_CHANNEL: - data = RequestChannelFrameFlyweight.data(frame); + data = RequestChannelFrameFlyweight.data(frame).retain(); break; // Payload and synthetic types case PAYLOAD: case NEXT: case NEXT_COMPLETE: case COMPLETE: - data = PayloadFrameFlyweight.data(frame); + data = PayloadFrameFlyweight.data(frame).retain(); break; default: throw new IllegalStateException("unsupported fragment type"); @@ -243,10 +243,10 @@ void reassembleFrame(ByteBuf frame, SynchronousSink sink) { boolean hasFollows = FrameHeaderFlyweight.hasFollows(frame); - if (!hasFollows) { - handleNoFollowsFlag(frame, sink, streamId); - } else { + if (hasFollows) { handleFollowsFlag(frame, streamId, frameType); + } else { + handleNoFollowsFlag(frame, sink, streamId); } } catch (Throwable t) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java b/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java index 692dcb363..74186f1d1 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java @@ -11,7 +11,7 @@ class DefaultPayloadDecoder implements PayloadDecoder { @Override - public Payload apply(ByteBuf byteBuf) { + public synchronized Payload apply(ByteBuf byteBuf) { ByteBuf m; ByteBuf d; FrameType type = FrameHeaderFlyweight.frameType(byteBuf); diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java index 19c0f1f62..a16f6d28d 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java @@ -61,7 +61,9 @@ final class FragmentationDuplexConnectionTest { void constructorInvalidMaxFragmentSize() { assertThatIllegalArgumentException() .isThrownBy( - () -> new FragmentationDuplexConnection(delegate, allocator, Integer.MIN_VALUE, false)) + () -> + new FragmentationDuplexConnection( + delegate, allocator, Integer.MIN_VALUE, false, "")) .withMessage("smallest allowed mtu size is 64 bytes"); } @@ -69,7 +71,7 @@ void constructorInvalidMaxFragmentSize() { @Test void constructorMtuLessThanMin() { assertThatIllegalArgumentException() - .isThrownBy(() -> new FragmentationDuplexConnection(delegate, allocator, 2, false)) + .isThrownBy(() -> new FragmentationDuplexConnection(delegate, allocator, 2, false, "")) .withMessage("smallest allowed mtu size is 64 bytes"); } @@ -77,7 +79,7 @@ void constructorMtuLessThanMin() { @Test void constructorNullByteBufAllocator() { assertThatNullPointerException() - .isThrownBy(() -> new FragmentationDuplexConnection(delegate, null, 64, false)) + .isThrownBy(() -> new FragmentationDuplexConnection(delegate, null, 64, false, "")) .withMessage("byteBufAllocator must not be null"); } @@ -85,7 +87,7 @@ void constructorNullByteBufAllocator() { @Test void constructorNullDelegate() { assertThatNullPointerException() - .isThrownBy(() -> new FragmentationDuplexConnection(null, allocator, 64, false)) + .isThrownBy(() -> new FragmentationDuplexConnection(null, allocator, 64, false, "")) .withMessage("delegate must not be null"); } @@ -118,7 +120,7 @@ void reassembleData() { when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); when(delegate.onClose()).thenReturn(Mono.never()); - new FragmentationDuplexConnection(delegate, allocator, 1030, false) + new FragmentationDuplexConnection(delegate, allocator, 1030, false, "") .receive() .as(StepVerifier::create) .assertNext( @@ -181,7 +183,7 @@ void reassembleMetadata() { when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); when(delegate.onClose()).thenReturn(Mono.never()); - new FragmentationDuplexConnection(delegate, allocator, 1030, false) + new FragmentationDuplexConnection(delegate, allocator, 1030, false, "") .receive() .as(StepVerifier::create) .assertNext( @@ -249,7 +251,7 @@ void reassembleMetadataAndData() { when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); when(delegate.onClose()).thenReturn(Mono.never()); - new FragmentationDuplexConnection(delegate, allocator, 1030, false) + new FragmentationDuplexConnection(delegate, allocator, 1030, false, "") .receive() .as(StepVerifier::create) .assertNext( @@ -270,7 +272,7 @@ void reassembleNonFragment() { when(delegate.receive()).thenReturn(Flux.just(encode)); when(delegate.onClose()).thenReturn(Mono.never()); - new FragmentationDuplexConnection(delegate, allocator, 1030, false) + new FragmentationDuplexConnection(delegate, allocator, 1030, false, "") .receive() .as(StepVerifier::create) .assertNext( @@ -289,7 +291,7 @@ void reassembleNonFragmentableFrame() { when(delegate.receive()).thenReturn(Flux.just(encode)); when(delegate.onClose()).thenReturn(Mono.never()); - new FragmentationDuplexConnection(delegate, allocator, 1030, false) + new FragmentationDuplexConnection(delegate, allocator, 1030, false, "") .receive() .as(StepVerifier::create) .assertNext( @@ -308,7 +310,7 @@ void sendData() { when(delegate.onClose()).thenReturn(Mono.never()); - new FragmentationDuplexConnection(delegate, allocator, 64, false).sendOne(encode.retain()); + new FragmentationDuplexConnection(delegate, allocator, 64, false, "").sendOne(encode.retain()); verify(delegate).send(publishers.capture()); diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java index d49ae43b4..e41d9e7db 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java @@ -81,7 +81,7 @@ public Mono connect(int mtu) { return connect.map( duplexConnection -> new FragmentationDuplexConnection( - duplexConnection, ByteBufAllocator.DEFAULT, mtu, false)); + duplexConnection, ByteBufAllocator.DEFAULT, mtu, false, "client")); } else { return connect; } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java index c1850b81c..f649d30ce 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java @@ -164,7 +164,7 @@ public void accept(DuplexConnection duplexConnection) { if (mtu > 0) { duplexConnection = new FragmentationDuplexConnection( - duplexConnection, ByteBufAllocator.DEFAULT, mtu, false); + duplexConnection, ByteBufAllocator.DEFAULT, mtu, false, "server"); } acceptor.apply(duplexConnection).subscribe(); diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java index 7c1070317..250d2eee0 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java @@ -101,7 +101,11 @@ public Mono connect(int mtu) { c -> { if (mtu > 0) { return new FragmentationDuplexConnection( - new TcpDuplexConnection(c, false), ByteBufAllocator.DEFAULT, mtu, true); + new TcpDuplexConnection(c, false), + ByteBufAllocator.DEFAULT, + mtu, + true, + "client"); } else { return new TcpDuplexConnection(c); } 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 002a2e13c..0da5b04d8 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 @@ -162,7 +162,7 @@ public Mono connect(int mtu) { if (mtu > 0) { connection = new FragmentationDuplexConnection( - connection, ByteBufAllocator.DEFAULT, mtu, false); + connection, ByteBufAllocator.DEFAULT, mtu, false, "client"); } return connection; }); diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java index 11adae8a6..b7f60aa6c 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java @@ -103,7 +103,11 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { if (mtu > 0) { connection = new FragmentationDuplexConnection( - new TcpDuplexConnection(c, false), ByteBufAllocator.DEFAULT, mtu, true); + new TcpDuplexConnection(c, false), + ByteBufAllocator.DEFAULT, + mtu, + true, + "server"); } else { connection = new TcpDuplexConnection(c); } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java index 8d9a3b137..9b78ece60 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java @@ -113,7 +113,8 @@ public static BiFunction> n DuplexConnection connection = new WebsocketDuplexConnection((Connection) in); if (mtu > 0) { connection = - new FragmentationDuplexConnection(connection, ByteBufAllocator.DEFAULT, mtu, false); + new FragmentationDuplexConnection( + connection, ByteBufAllocator.DEFAULT, mtu, false, "server"); } return acceptor.apply(connection).then(out.neverComplete()); }; diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java index 8e9ba377d..4ac68cdfd 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java @@ -123,7 +123,7 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { if (mtu > 0) { connection = new FragmentationDuplexConnection( - connection, ByteBufAllocator.DEFAULT, mtu, false); + connection, ByteBufAllocator.DEFAULT, mtu, false, "server"); } return acceptor.apply(connection).then(out.neverComplete()); }); diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java index 61a6d0f20..62d7da336 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java @@ -35,23 +35,28 @@ import reactor.core.publisher.Mono; public class FragmentTest { - private static final int frameSize = 128; + private static final int frameSize = 64; private AbstractRSocket handler; private CloseableChannel server; private String message = null; private String metaData = null; + private String responseMessage = null; @BeforeEach public void startup() { int randomPort = ThreadLocalRandom.current().nextInt(10_000, 20_000); StringBuilder message = new StringBuilder(); + StringBuilder responseMessage = new StringBuilder(); StringBuilder metaData = new StringBuilder(); for (int i = 0; i < 100; i++) { - message.append("RESPONSE "); + message.append("REQUEST "); + responseMessage.append("RESPONSE "); metaData.append("METADATA "); } this.message = message.toString(); + this.responseMessage = responseMessage.toString(); this.metaData = metaData.toString(); + TcpServerTransport serverTransport = TcpServerTransport.create(randomPort); server = RSocketFactory.receive() @@ -88,7 +93,7 @@ public Flux requestStream(Payload payload) { System.out.println("request message: " + request); System.out.println("request metadata: " + metaData); - return Flux.just(DefaultPayload.create(request)); + return Flux.just(DefaultPayload.create(responseMessage)); } }; @@ -100,7 +105,7 @@ public Flux requestStream(Payload payload) { System.out.println("response message: " + payload.getDataUtf8()); System.out.println("response metadata: " + payload.getMetadataUtf8()); - assertThat(message).isEqualTo(payload.getDataUtf8()); + assertThat(responseMessage).isEqualTo(payload.getDataUtf8()); } @Test @@ -116,7 +121,7 @@ public Flux requestStream(Payload payload) { System.out.println("request message: " + request); System.out.println("request metadata: " + metaData); - return Flux.just(DefaultPayload.create(request)); + return Flux.just(DefaultPayload.create(responseMessage)); } }; @@ -128,11 +133,12 @@ public Flux requestStream(Payload payload) { System.out.println("response message: " + payload.getDataUtf8()); System.out.println("response metadata: " + payload.getMetadataUtf8()); - assertThat(message).isEqualTo(payload.getDataUtf8()); + assertThat(responseMessage).isEqualTo(payload.getDataUtf8()); } @Test void testFragmentBothMetaData() { + Payload responsePayload = DefaultPayload.create(responseMessage); System.out.println( "-------------------------------------------------testFragmentBothMetaData-------------------------------------------------"); handler = @@ -144,7 +150,7 @@ public Flux requestStream(Payload payload) { System.out.println("request message: " + request); System.out.println("request metadata: " + metaData); - return Flux.just(DefaultPayload.create(request, metaData)); + return Flux.just(DefaultPayload.create(responseMessage, metaData)); } @Override @@ -154,7 +160,7 @@ public Mono requestResponse(Payload payload) { System.out.println("request message: " + request); System.out.println("request metadata: " + metaData); - return Mono.just(DefaultPayload.create(request, metaData)); + return Mono.just(DefaultPayload.create(responseMessage, metaData)); } }; @@ -166,6 +172,6 @@ public Mono requestResponse(Payload payload) { System.out.println("response message: " + payload.getDataUtf8()); System.out.println("response metadata: " + payload.getMetadataUtf8()); - assertThat(message).isEqualTo(payload.getDataUtf8()); + assertThat(responseMessage).isEqualTo(payload.getDataUtf8()); } } diff --git a/rsocket-transport-netty/src/test/resources/logback-test.xml b/rsocket-transport-netty/src/test/resources/logback-test.xml index 7150e3f0f..e8eb89467 100644 --- a/rsocket-transport-netty/src/test/resources/logback-test.xml +++ b/rsocket-transport-netty/src/test/resources/logback-test.xml @@ -25,6 +25,7 @@ + From 102590efb48e9de9359aba984ac8a4dd6f054fb3 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 15 Apr 2019 20:52:25 +0300 Subject: [PATCH 024/181] provides final tuple bytebuf implementation (#614) Signed-off-by: Oleh Dokuka --- .../rsocket/buffer/AbstractTupleByteBuf.java | 552 +++++++++++++++++ .../java/io/rsocket/buffer/BufferUtil.java | 78 +++ .../java/io/rsocket/buffer/Tuple2ByteBuf.java | 394 ++++++++++++ .../java/io/rsocket/buffer/Tuple3ByteBuf.java | 580 ++++++++++++++++++ .../io/rsocket/buffer/TupleByteBuffs.java | 35 ++ .../frame/DataAndMetadataFlyweight.java | 11 +- .../frame/ExtensionFrameFlyweight.java | 4 +- .../rsocket/frame/FragmentationFlyweight.java | 9 +- .../rsocket/frame/FrameLengthFlyweight.java | 3 +- .../java/io/rsocket/frame/LeaseFlyweight.java | 6 +- .../io/rsocket/frame/RequestFlyweight.java | 4 +- .../io/rsocket/frame/SetupFrameFlyweight.java | 8 +- .../FragmentationIntegrationTest.java | 2 + .../frame/DataAndMetadataFlyweightTest.java | 51 ++ .../internal/SwitchTransformFluxTest.java | 2 + .../transport/netty/WebsocketPing.java | 6 +- .../transport/netty/WebsocketPongServer.java | 2 + 17 files changed, 1732 insertions(+), 15 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java create mode 100644 rsocket-core/src/main/java/io/rsocket/buffer/BufferUtil.java create mode 100644 rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java create mode 100644 rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java create mode 100644 rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuffs.java create mode 100644 rsocket-core/src/test/java/io/rsocket/frame/DataAndMetadataFlyweightTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java new file mode 100644 index 000000000..7049a97ba --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java @@ -0,0 +1,552 @@ +package io.rsocket.buffer; + +import io.netty.buffer.AbstractReferenceCountedByteBuf; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.internal.SystemPropertyUtil; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; +import java.nio.channels.ScatteringByteChannel; +import java.nio.charset.Charset; + +abstract class AbstractTupleByteBuf extends AbstractReferenceCountedByteBuf { + static final int DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT = + SystemPropertyUtil.getInt("io.netty.allocator.directMemoryCacheAlignment", 0); + static final ByteBuffer EMPTY_NIO_BUFFER = Unpooled.EMPTY_BUFFER.nioBuffer(); + + final ByteBufAllocator allocator; + final int capacity; + + AbstractTupleByteBuf(ByteBufAllocator allocator, int capacity) { + super(Integer.MAX_VALUE); + + this.capacity = capacity; + this.allocator = allocator; + super.writerIndex(capacity); + } + + abstract long calculateRelativeIndex(int index); + + abstract ByteBuf getPart(int index); + + @Override + public ByteBuffer nioBuffer(int index, int length) { + checkIndex(index, length); + + ByteBuffer[] buffers = nioBuffers(index, length); + + if (buffers.length == 1) { + return buffers[0].duplicate(); + } + + ByteBuffer merged = + BufferUtil.allocateDirectAligned(length, DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT) + .order(order()); + for (ByteBuffer buf : buffers) { + merged.put(buf); + } + + merged.flip(); + return merged; + } + + @Override + protected byte _getByte(int index) { + long ri = calculateRelativeIndex(index); + ByteBuf byteBuf = getPart(index); + + index = (int) (ri & Integer.MAX_VALUE); + + return byteBuf.getByte(index); + } + + @Override + protected short _getShort(int index) { + long ri = calculateRelativeIndex(index); + ByteBuf byteBuf = getPart(index); + + index = (int) (ri & Integer.MAX_VALUE); + + return byteBuf.getShort(index); + } + + @Override + protected short _getShortLE(int index) { + long ri = calculateRelativeIndex(index); + ByteBuf byteBuf = getPart(index); + + index = (int) (ri & Integer.MAX_VALUE); + + return byteBuf.getShortLE(index); + } + + @Override + protected int _getUnsignedMedium(int index) { + long ri = calculateRelativeIndex(index); + ByteBuf byteBuf = getPart(index); + + index = (int) (ri & Integer.MAX_VALUE); + + return byteBuf.getUnsignedMedium(index); + } + + @Override + protected int _getUnsignedMediumLE(int index) { + long ri = calculateRelativeIndex(index); + ByteBuf byteBuf = getPart(index); + + index = (int) (ri & Integer.MAX_VALUE); + + return byteBuf.getUnsignedMediumLE(index); + } + + @Override + protected int _getInt(int index) { + long ri = calculateRelativeIndex(index); + ByteBuf byteBuf = getPart(index); + + index = (int) (ri & Integer.MAX_VALUE); + + return byteBuf.getInt(index); + } + + @Override + protected int _getIntLE(int index) { + long ri = calculateRelativeIndex(index); + ByteBuf byteBuf = getPart(index); + + index = (int) (ri & Integer.MAX_VALUE); + + return byteBuf.getIntLE(index); + } + + @Override + protected long _getLong(int index) { + long ri = calculateRelativeIndex(index); + ByteBuf byteBuf = getPart(index); + + index = (int) (ri & Integer.MAX_VALUE); + + return byteBuf.getLong(index); + } + + @Override + protected long _getLongLE(int index) { + long ri = calculateRelativeIndex(index); + ByteBuf byteBuf = getPart(index); + + index = (int) (ri & Integer.MAX_VALUE); + + return byteBuf.getLongLE(index); + } + + @Override + public ByteBufAllocator alloc() { + return allocator; + } + + @Override + public int capacity() { + return capacity; + } + + @Override + public ByteBuf capacity(int newCapacity) { + throw new UnsupportedOperationException(); + } + + @Override + public int maxCapacity() { + return capacity; + } + + @Override + public ByteOrder order() { + return ByteOrder.LITTLE_ENDIAN; + } + + @Override + public ByteBuf order(ByteOrder endianness) { + return this; + } + + @Override + public ByteBuf unwrap() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isReadOnly() { + return true; + } + + @Override + public ByteBuf asReadOnly() { + return this; + } + + @Override + public boolean isWritable() { + return false; + } + + @Override + public boolean isWritable(int size) { + return false; + } + + @Override + public ByteBuf writerIndex(int writerIndex) { + return this; + } + + @Override + public final int writerIndex() { + return capacity; + } + + @Override + public ByteBuf setIndex(int readerIndex, int writerIndex) { + return this; + } + + @Override + public ByteBuf clear() { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf discardReadBytes() { + return this; + } + + @Override + public ByteBuf discardSomeReadBytes() { + return this; + } + + @Override + public ByteBuf ensureWritable(int minWritableBytes) { + return this; + } + + @Override + public int ensureWritable(int minWritableBytes, boolean force) { + return 0; + } + + @Override + public ByteBuf setFloatLE(int index, float value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setDoubleLE(int index, double value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setBoolean(int index, boolean value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setByte(int index, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setShort(int index, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setShortLE(int index, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setMedium(int index, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setMediumLE(int index, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setInt(int index, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setIntLE(int index, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setLong(int index, long value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setLongLE(int index, long value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setChar(int index, int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setFloat(int index, float value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setDouble(int index, double value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setBytes(int index, ByteBuf src) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setBytes(int index, ByteBuf src, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setBytes(int index, ByteBuf src, int srcIndex, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setBytes(int index, byte[] src) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setBytes(int index, ByteBuffer src) { + throw new UnsupportedOperationException(); + } + + @Override + public int setBytes(int index, InputStream in, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public int setBytes(int index, ScatteringByteChannel in, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public int setBytes(int index, FileChannel in, long position, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public int setCharSequence(int index, CharSequence sequence, Charset charset) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf setZero(int index, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeBoolean(boolean value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeByte(int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeShort(int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeShortLE(int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeMedium(int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeMediumLE(int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeInt(int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeIntLE(int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeLong(long value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeLongLE(long value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeChar(int value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeFloat(float value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeDouble(double value) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeBytes(ByteBuf src) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeBytes(ByteBuf src, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeBytes(ByteBuf src, int srcIndex, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeBytes(byte[] src) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeBytes(byte[] src, int srcIndex, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeBytes(ByteBuffer src) { + throw new UnsupportedOperationException(); + } + + @Override + public int writeBytes(InputStream in, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public int writeBytes(ScatteringByteChannel in, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public int writeBytes(FileChannel in, long position, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuf writeZero(int length) { + throw new UnsupportedOperationException(); + } + + @Override + public int writeCharSequence(CharSequence sequence, Charset charset) { + throw new UnsupportedOperationException(); + } + + @Override + public ByteBuffer internalNioBuffer(int index, int length) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasArray() { + return false; + } + + @Override + public byte[] array() { + throw new UnsupportedOperationException(); + } + + @Override + public int arrayOffset() { + throw new UnsupportedOperationException(); + } + + @Override + public boolean hasMemoryAddress() { + return false; + } + + @Override + public long memoryAddress() { + throw new UnsupportedOperationException(); + } + + @Override + public int compareTo(ByteBuf buffer) { + return 0; + } + + @Override + protected void _setByte(int index, int value) {} + + @Override + protected void _setShort(int index, int value) {} + + @Override + protected void _setShortLE(int index, int value) {} + + @Override + protected void _setMedium(int index, int value) {} + + @Override + protected void _setMediumLE(int index, int value) {} + + @Override + protected void _setInt(int index, int value) {} + + @Override + protected void _setIntLE(int index, int value) {} + + @Override + protected void _setLong(int index, long value) {} + + @Override + protected void _setLongLE(int index, long value) {} +} diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/BufferUtil.java b/rsocket-core/src/main/java/io/rsocket/buffer/BufferUtil.java new file mode 100644 index 000000000..476583ab3 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/buffer/BufferUtil.java @@ -0,0 +1,78 @@ +package io.rsocket.buffer; + +import java.lang.reflect.Field; +import java.nio.Buffer; +import java.nio.ByteBuffer; +import java.security.AccessController; +import java.security.PrivilegedExceptionAction; +import sun.misc.Unsafe; + +abstract class BufferUtil { + + private static final Unsafe UNSAFE; + + static { + Unsafe unsafe; + try { + final PrivilegedExceptionAction action = + () -> { + final Field f = Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + + return (Unsafe) f.get(null); + }; + + unsafe = AccessController.doPrivileged(action); + } catch (final Exception ex) { + throw new RuntimeException(ex); + } + + UNSAFE = unsafe; + } + + private static final long BYTE_BUFFER_ADDRESS_FIELD_OFFSET; + + static { + try { + BYTE_BUFFER_ADDRESS_FIELD_OFFSET = + UNSAFE.objectFieldOffset(Buffer.class.getDeclaredField("address")); + } catch (final Exception ex) { + throw new RuntimeException(ex); + } + } + + /** + * Allocate a new direct {@link ByteBuffer} that is aligned on a given alignment boundary. + * + * @param capacity required for the buffer. + * @param alignment boundary at which the buffer should begin. + * @return a new {@link ByteBuffer} with the required alignment. + * @throws IllegalArgumentException if the alignment is not a power of 2. + */ + static ByteBuffer allocateDirectAligned(final int capacity, final int alignment) { + if (alignment == 0) { + return ByteBuffer.allocateDirect(capacity); + } + + if (!isPowerOfTwo(alignment)) { + throw new IllegalArgumentException("Must be a power of 2: alignment=" + alignment); + } + + final ByteBuffer buffer = ByteBuffer.allocateDirect(capacity + alignment); + + final long address = UNSAFE.getLong(buffer, BYTE_BUFFER_ADDRESS_FIELD_OFFSET); + final int remainder = (int) (address & (alignment - 1)); + final int offset = alignment - remainder; + + buffer.limit(capacity + offset); + buffer.position(offset); + + return buffer.slice(); + } + + private static boolean isPowerOfTwo(final int value) { + return value > 0 && ((value & (~value + 1)) == value); + } + + private BufferUtil() {} +} diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java new file mode 100644 index 000000000..278e0ce66 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java @@ -0,0 +1,394 @@ +package io.rsocket.buffer; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCountUtil; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.GatheringByteChannel; +import java.nio.charset.Charset; + +class Tuple2ByteBuf extends AbstractTupleByteBuf { + + private static final long ONE_MASK = 0x100000000L; + private static final long TWO_MASK = 0x200000000L; + private static final long MASK = 0x700000000L; + + private final ByteBuf one; + private final ByteBuf two; + private final int oneReadIndex; + private final int twoReadIndex; + private final int oneReadableBytes; + private final int twoReadableBytes; + private final int twoRelativeIndex; + + private boolean freed; + + Tuple2ByteBuf(ByteBufAllocator allocator, ByteBuf one, ByteBuf two) { + super(allocator, one.readableBytes() + two.readableBytes()); + + this.one = one; + this.two = two; + + this.oneReadIndex = one.readerIndex(); + this.twoReadIndex = two.readerIndex(); + + this.oneReadableBytes = one.readableBytes(); + this.twoReadableBytes = two.readableBytes(); + + this.twoRelativeIndex = oneReadableBytes; + + this.freed = false; + } + + long calculateRelativeIndex(int index) { + checkIndex(index, 0); + + long relativeIndex; + long mask; + if (index >= twoRelativeIndex) { + relativeIndex = twoReadIndex + (index - oneReadableBytes); + mask = TWO_MASK; + } else { + relativeIndex = oneReadIndex + index; + mask = ONE_MASK; + } + + return relativeIndex | mask; + } + + ByteBuf getPart(int index) { + long ri = calculateRelativeIndex(index); + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + return one; + case 0x2: + return two; + default: + throw new IllegalStateException(); + } + } + + @Override + public boolean isDirect() { + return one.isDirect() && two.isDirect(); + } + + @Override + public int nioBufferCount() { + return one.nioBufferCount() + two.nioBufferCount(); + } + + @Override + public ByteBuffer nioBuffer() { + ByteBuffer[] oneBuffers = one.nioBuffers(); + ByteBuffer[] twoBuffers = two.nioBuffers(); + + ByteBuffer merged = + BufferUtil.allocateDirectAligned(capacity, DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT) + .order(order()); + + for (ByteBuffer b : oneBuffers) { + merged.put(b); + } + + for (ByteBuffer b : twoBuffers) { + merged.put(b); + } + + merged.flip(); + return merged; + } + + @Override + public ByteBuffer[] nioBuffers(int index, int length) { + if (length == 0) { + return new ByteBuffer[] {EMPTY_NIO_BUFFER}; + } + + long ri = calculateRelativeIndex(index); + index = (int) (ri & Integer.MAX_VALUE); + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + ByteBuffer[] oneBuffer; + ByteBuffer[] twoBuffer; + int l = Math.min(oneReadableBytes - index, length); + oneBuffer = one.nioBuffers(index, l); + length -= l; + if (length != 0) { + twoBuffer = two.nioBuffers(twoReadIndex, length); + ByteBuffer[] results = new ByteBuffer[oneBuffer.length + twoBuffer.length]; + System.arraycopy(oneBuffer, 0, results, 0, oneBuffer.length); + System.arraycopy(twoBuffer, 0, results, oneBuffer.length, twoBuffer.length); + return results; + } else { + return oneBuffer; + } + case 0x2: + return two.nioBuffers(index, length); + default: + throw new IllegalStateException(); + } + } + + @Override + public ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) { + long ri = calculateRelativeIndex(index); + index = (int) (ri & Integer.MAX_VALUE); + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + { + int l = Math.min(oneReadableBytes - index, length); + one.getBytes(index, dst, dstIndex, l); + length -= l; + dstIndex += l; + + if (length != 0) { + two.getBytes(twoReadIndex, dst, dstIndex, length); + } + + break; + } + case 0x2: + { + two.getBytes(index, dst, dstIndex, length); + break; + } + default: + throw new IllegalStateException(); + } + + return this; + } + + @Override + public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) { + ByteBuf dstBuf = Unpooled.wrappedBuffer(dst); + int min = Math.min(dst.length, capacity); + return getBytes(0, dstBuf, index, min); + } + + @Override + public ByteBuf getBytes(int index, ByteBuffer dst) { + ByteBuf dstBuf = Unpooled.wrappedBuffer(dst); + int min = Math.min(dst.limit(), capacity); + return getBytes(0, dstBuf, index, min); + } + + @Override + public ByteBuf getBytes(int index, final OutputStream out, int length) throws IOException { + checkIndex(index, length); + long ri = calculateRelativeIndex(index); + index = (int) (ri & Integer.MAX_VALUE); + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + { + int l = Math.min(oneReadableBytes - index, length); + one.getBytes(index, out, l); + length -= l; + if (length != 0) { + two.getBytes(twoReadIndex, out, length); + } + break; + } + case 0x2: + { + two.getBytes(index, out, length); + break; + } + default: + throw new IllegalStateException(); + } + + return this; + } + + @Override + public int getBytes(int index, GatheringByteChannel out, int length) throws IOException { + checkIndex(index, length); + int read = 0; + long ri = calculateRelativeIndex(index); + index = (int) (ri & Integer.MAX_VALUE); + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + { + int l = Math.min(oneReadableBytes - index, length); + read += one.getBytes(index, out, l); + length -= l; + if (length != 0) { + read += two.getBytes(twoReadIndex, out, length); + } + break; + } + case 0x2: + { + read += two.getBytes(index, out, length); + break; + } + default: + throw new IllegalStateException(); + } + + return read; + } + + @Override + public int getBytes(int index, FileChannel out, long position, int length) throws IOException { + checkIndex(index, length); + int read = 0; + long ri = calculateRelativeIndex(index); + index = (int) (ri & Integer.MAX_VALUE); + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + { + int l = Math.min(oneReadableBytes - index, length); + read += one.getBytes(index, out, position, l); + length -= l; + position += l; + if (length != 0) { + read += two.getBytes(twoReadIndex, out, position, length); + } + break; + } + case 0x2: + { + read += two.getBytes(index, out, position, length); + break; + } + default: + throw new IllegalStateException(); + } + + return read; + } + + @Override + public ByteBuf copy(int index, int length) { + checkIndex(index, length); + + ByteBuf buffer = allocator.buffer(length); + + if (index == 0 && length == capacity) { + buffer.writeBytes(one, oneReadIndex, oneReadableBytes); + buffer.writeBytes(two, twoReadIndex, twoReadableBytes); + + return buffer; + } + + long ri = calculateRelativeIndex(index); + index = (int) (ri & Integer.MAX_VALUE); + + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + { + int l = Math.min(oneReadableBytes - index, length); + buffer.writeBytes(one, index, l); + + length -= l; + + if (length != 0) { + buffer.writeBytes(two, twoReadIndex, length); + } + + return buffer; + } + case 0x2: + { + return buffer.writeBytes(two, index, length); + } + default: + throw new IllegalStateException(); + } + } + + @Override + public ByteBuf slice(final int readIndex, int length) { + checkIndex(readIndex, length); + + if (readIndex == 0 && length == capacity) { + return new Tuple2ByteBuf( + allocator, + one.slice(oneReadIndex, oneReadableBytes), + two.slice(twoReadIndex, twoReadableBytes)); + } + + long ri = calculateRelativeIndex(readIndex); + int index = (int) (ri & Integer.MAX_VALUE); + + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + { + ByteBuf oneSlice; + ByteBuf twoSlice; + + int l = Math.min(oneReadableBytes - index, length); + oneSlice = one.slice(index, l); + length -= l; + if (length != 0) { + twoSlice = two.slice(twoReadIndex, length); + return new Tuple2ByteBuf(allocator, oneSlice, twoSlice); + } else { + return oneSlice; + } + } + case 0x2: + { + return two.slice(index, length); + } + default: + throw new IllegalStateException(); + } + } + + @Override + protected void deallocate() { + if (freed) { + return; + } + + freed = true; + ReferenceCountUtil.safeRelease(one); + ReferenceCountUtil.safeRelease(two); + } + + @Override + public String toString(Charset charset) { + StringBuilder builder = new StringBuilder(3); + builder.append(one.toString(charset)); + builder.append(two.toString(charset)); + return builder.toString(); + } + + @Override + public String toString(int index, int length, Charset charset) { + // TODO - make this smarter + return toString(charset).substring(index, length); + } + + @Override + public String toString() { + return "Tuple2ByteBuf{" + + "capacity=" + + capacity + + ", one=" + + one + + ", two=" + + two + + ", allocator=" + + allocator + + ", oneReadIndex=" + + oneReadIndex + + ", twoReadIndex=" + + twoReadIndex + + ", oneReadableBytes=" + + oneReadableBytes + + ", twoReadableBytes=" + + twoReadableBytes + + ", twoRelativeIndex=" + + twoRelativeIndex + + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java new file mode 100644 index 000000000..4871c52ae --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java @@ -0,0 +1,580 @@ +package io.rsocket.buffer; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.ReferenceCountUtil; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.GatheringByteChannel; +import java.nio.charset.Charset; + +class Tuple3ByteBuf extends AbstractTupleByteBuf { + private static final long ONE_MASK = 0x100000000L; + private static final long TWO_MASK = 0x200000000L; + private static final long THREE_MASK = 0x400000000L; + private static final long MASK = 0x700000000L; + + private final ByteBuf one; + private final ByteBuf two; + private final ByteBuf three; + private final int oneReadIndex; + private final int twoReadIndex; + private final int threeReadIndex; + private final int oneReadableBytes; + private final int twoReadableBytes; + private final int threeReadableBytes; + private final int twoRelativeIndex; + private final int threeRelativeIndex; + + private boolean freed; + + Tuple3ByteBuf(ByteBufAllocator allocator, ByteBuf one, ByteBuf two, ByteBuf three) { + super(allocator, one.readableBytes() + two.readableBytes() + three.readableBytes()); + + this.one = one; + this.two = two; + this.three = three; + + this.oneReadIndex = one.readerIndex(); + this.twoReadIndex = two.readerIndex(); + this.threeReadIndex = three.readerIndex(); + + this.oneReadableBytes = one.readableBytes(); + this.twoReadableBytes = two.readableBytes(); + this.threeReadableBytes = three.readableBytes(); + + this.twoRelativeIndex = oneReadableBytes; + this.threeRelativeIndex = twoRelativeIndex + twoReadableBytes; + + this.freed = false; + } + + @Override + public boolean isDirect() { + return one.isDirect() && two.isDirect() && three.isDirect(); + } + + public long calculateRelativeIndex(int index) { + checkIndex(index, 0); + long relativeIndex; + long mask; + if (index >= threeRelativeIndex) { + relativeIndex = threeReadIndex + (index - twoReadableBytes - oneReadableBytes); + mask = THREE_MASK; + } else if (index >= twoRelativeIndex) { + relativeIndex = twoReadIndex + (index - oneReadableBytes); + mask = TWO_MASK; + } else { + relativeIndex = oneReadIndex + index; + mask = ONE_MASK; + } + + return relativeIndex | mask; + } + + public ByteBuf getPart(int index) { + long ri = calculateRelativeIndex(index); + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + return one; + case 0x2: + return two; + case 0x4: + return three; + default: + throw new IllegalStateException(); + } + } + + @Override + public int nioBufferCount() { + return one.nioBufferCount() + two.nioBufferCount() + three.nioBufferCount(); + } + + @Override + public ByteBuffer nioBuffer() { + + ByteBuffer[] oneBuffers = one.nioBuffers(); + ByteBuffer[] twoBuffers = two.nioBuffers(); + ByteBuffer[] threeBuffers = three.nioBuffers(); + + ByteBuffer merged = + BufferUtil.allocateDirectAligned(capacity, DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT) + .order(order()); + + for (ByteBuffer b : oneBuffers) { + merged.put(b); + } + + for (ByteBuffer b : twoBuffers) { + merged.put(b); + } + + for (ByteBuffer b : threeBuffers) { + merged.put(b); + } + + merged.flip(); + return merged; + } + + @Override + public ByteBuffer[] nioBuffers(int index, int length) { + if (length == 0) { + return new ByteBuffer[] {EMPTY_NIO_BUFFER}; + } + + long ri = calculateRelativeIndex(index); + index = (int) (ri & Integer.MAX_VALUE); + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + { + ByteBuffer[] oneBuffer; + ByteBuffer[] twoBuffer; + ByteBuffer[] threeBuffer; + int l = Math.min(oneReadableBytes - index, length); + oneBuffer = one.nioBuffers(index, l); + length -= l; + if (length != 0) { + l = Math.min(twoReadableBytes, length); + twoBuffer = two.nioBuffers(twoReadIndex, l); + length -= l; + if (length != 0) { + threeBuffer = three.nioBuffers(threeReadIndex, length); + ByteBuffer[] results = + new ByteBuffer[oneBuffer.length + twoBuffer.length + threeBuffer.length]; + System.arraycopy(oneBuffer, 0, results, 0, oneBuffer.length); + System.arraycopy(twoBuffer, 0, results, oneBuffer.length, twoBuffer.length); + System.arraycopy(threeBuffer, 0, results, twoBuffer.length, threeBuffer.length); + return results; + } else { + ByteBuffer[] results = new ByteBuffer[oneBuffer.length + twoBuffer.length]; + System.arraycopy(oneBuffer, 0, results, 0, oneBuffer.length); + System.arraycopy(twoBuffer, 0, results, oneBuffer.length, twoBuffer.length); + return results; + } + } else { + return oneBuffer; + } + } + case 0x2: + { + ByteBuffer[] twoBuffer; + ByteBuffer[] threeBuffer; + int l = Math.min(twoReadableBytes - index, length); + twoBuffer = two.nioBuffers(index, length); + length -= l; + if (length != 0) { + threeBuffer = three.nioBuffers(threeReadIndex, length); + ByteBuffer[] results = new ByteBuffer[twoBuffer.length + threeBuffer.length]; + System.arraycopy(twoBuffer, 0, results, 0, twoBuffer.length); + System.arraycopy(threeBuffer, 0, results, threeBuffer.length, twoBuffer.length); + return results; + } else { + return twoBuffer; + } + } + case 0x4: + return three.nioBuffers(index, length); + default: + throw new IllegalStateException(); + } + } + + @Override + public ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) { + checkDstIndex(index, length, dstIndex, dst.capacity()); + long ri = calculateRelativeIndex(index); + index = (int) (ri & Integer.MAX_VALUE); + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + { + int l = Math.min(oneReadableBytes - index, length); + one.getBytes(index, dst, dstIndex, l); + length -= l; + dstIndex += l; + + if (length != 0) { + l = Math.min(twoReadableBytes, length); + two.getBytes(twoReadIndex, dst, dstIndex, l); + length -= l; + dstIndex += l; + + if (length != 0) { + three.getBytes(threeReadIndex, dst, dstIndex, length); + } + } + break; + } + case 0x2: + { + int l = Math.min(twoReadableBytes - index, length); + two.getBytes(index, dst, dstIndex, l); + length -= l; + dstIndex += l; + + if (length != 0) { + three.getBytes(threeReadIndex, dst, dstIndex, length); + } + break; + } + case 0x4: + { + three.getBytes(index, dst, dstIndex, length); + break; + } + default: + throw new IllegalStateException(); + } + + return this; + } + + @Override + public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) { + ByteBuf dstBuf = Unpooled.wrappedBuffer(dst); + int min = Math.min(dst.length, capacity); + return getBytes(0, dstBuf, index, min); + } + + @Override + public ByteBuf getBytes(int index, ByteBuffer dst) { + ByteBuf dstBuf = Unpooled.wrappedBuffer(dst); + int min = Math.min(dst.limit(), capacity); + return getBytes(0, dstBuf, index, min); + } + + @Override + public ByteBuf getBytes(int index, final OutputStream out, int length) throws IOException { + checkIndex(index, length); + long ri = calculateRelativeIndex(index); + index = (int) (ri & Integer.MAX_VALUE); + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + { + int l = Math.min(oneReadableBytes - index, length); + one.getBytes(index, out, l); + length -= l; + if (length != 0) { + l = Math.min(twoReadableBytes, length); + two.getBytes(twoReadIndex, out, l); + length -= l; + if (length != 0) { + three.getBytes(threeReadIndex, out, length); + } + } + break; + } + case 0x2: + { + int l = Math.min(twoReadableBytes - index, length); + two.getBytes(index, out, l); + length -= l; + + if (length != 0) { + three.getBytes(threeReadIndex, out, length); + } + break; + } + case 0x4: + { + three.getBytes(index, out, length); + + break; + } + default: + throw new IllegalStateException(); + } + + return this; + } + + @Override + public int getBytes(int index, GatheringByteChannel out, int length) throws IOException { + checkIndex(index, length); + int read = 0; + long ri = calculateRelativeIndex(index); + index = (int) (ri & Integer.MAX_VALUE); + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + { + int l = Math.min(oneReadableBytes - index, length); + read += one.getBytes(index, out, l); + length -= l; + if (length != 0) { + l = Math.min(twoReadableBytes, length); + read += two.getBytes(twoReadIndex, out, l); + length -= l; + if (length != 0) { + read += three.getBytes(threeReadIndex, out, length); + } + } + break; + } + case 0x2: + { + int l = Math.min(twoReadableBytes - index, length); + read += two.getBytes(index, out, l); + length -= l; + + if (length != 0) { + read += three.getBytes(threeReadIndex, out, length); + } + break; + } + case 0x4: + { + read += three.getBytes(index, out, length); + + break; + } + default: + throw new IllegalStateException(); + } + + return read; + } + + @Override + public int getBytes(int index, FileChannel out, long position, int length) throws IOException { + checkIndex(index, length); + int read = 0; + long ri = calculateRelativeIndex(index); + index = (int) (ri & Integer.MAX_VALUE); + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + { + int l = Math.min(oneReadableBytes - index, length); + read += one.getBytes(index, out, position, l); + length -= l; + position += l; + + if (length != 0) { + l = Math.min(twoReadableBytes, length); + read += two.getBytes(twoReadIndex, out, position, l); + length -= l; + position += l; + + if (length != 0) { + read += three.getBytes(threeReadIndex, out, position, length); + } + } + break; + } + case 0x2: + { + int l = Math.min(twoReadableBytes - index, length); + read += two.getBytes(index, out, position, l); + length -= l; + position += l; + + if (length != 0) { + read += three.getBytes(threeReadIndex, out, position, length); + } + break; + } + case 0x4: + { + read += three.getBytes(index, out, position, length); + + break; + } + default: + throw new IllegalStateException(); + } + + return read; + } + + @Override + public ByteBuf copy(int index, int length) { + checkIndex(index, length); + + ByteBuf buffer = allocator.buffer(length); + + if (index == 0 && length == capacity) { + buffer.writeBytes(one, oneReadIndex, oneReadableBytes); + buffer.writeBytes(two, twoReadIndex, twoReadableBytes); + buffer.writeBytes(three, threeReadIndex, threeReadableBytes); + + return buffer; + } + + long ri = calculateRelativeIndex(index); + index = (int) (ri & Integer.MAX_VALUE); + + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + { + int l = Math.min(oneReadableBytes - index, length); + buffer.writeBytes(one, index, l); + length -= l; + + if (length != 0) { + l = Math.min(twoReadableBytes, length); + buffer.writeBytes(two, twoReadIndex, l); + length -= l; + if (length != 0) { + buffer.writeBytes(three, threeReadIndex, length); + } + } + + return buffer; + } + case 0x2: + { + int l = Math.min(twoReadableBytes - index, length); + buffer.writeBytes(two, index, l); + length -= l; + + if (length != 0) { + buffer.writeBytes(three, threeReadIndex, length); + } + + return buffer; + } + case 0x4: + { + buffer.writeBytes(three, index, length); + + return buffer; + } + default: + throw new IllegalStateException(); + } + } + + @Override + public ByteBuf retainedSlice() { + return new Tuple3ByteBuf( + allocator, + one.retainedSlice(oneReadIndex, oneReadableBytes), + two.retainedSlice(twoReadIndex, twoReadableBytes), + three.retainedSlice(threeReadIndex, threeReadableBytes)); + } + + @Override + public ByteBuf slice(final int readIndex, int length) { + checkIndex(readIndex, length); + + if (readIndex == 0 && length == capacity) { + return new Tuple3ByteBuf( + allocator, + one.slice(oneReadIndex, oneReadableBytes), + two.slice(twoReadIndex, twoReadableBytes), + three.slice(threeReadIndex, threeReadableBytes)); + } + + long ri = calculateRelativeIndex(readIndex); + int index = (int) (ri & Integer.MAX_VALUE); + switch ((int) ((ri & MASK) >>> 32L)) { + case 0x1: + { + ByteBuf oneSlice; + ByteBuf twoSlice; + ByteBuf threeSlice; + + int l = Math.min(oneReadableBytes - index, length); + oneSlice = one.slice(index, l); + length -= l; + if (length != 0) { + l = Math.min(twoReadableBytes, length); + twoSlice = two.slice(twoReadIndex, l); + length -= l; + if (length != 0) { + threeSlice = three.slice(threeReadIndex, length); + return new Tuple3ByteBuf(allocator, oneSlice, twoSlice, threeSlice); + } else { + return new Tuple2ByteBuf(allocator, oneSlice, twoSlice); + } + + } else { + return oneSlice; + } + } + case 0x2: + { + ByteBuf twoSlice; + ByteBuf threeSlice; + + int l = Math.min(twoReadableBytes - index, length); + twoSlice = two.slice(index, l); + length -= l; + if (length != 0) { + threeSlice = three.slice(threeReadIndex, length); + return new Tuple2ByteBuf(allocator, twoSlice, threeSlice); + } else { + return twoSlice; + } + } + case 0x4: + { + return three.slice(index, length); + } + default: + throw new IllegalStateException(); + } + } + + @Override + protected void deallocate() { + if (freed) { + return; + } + + freed = true; + ReferenceCountUtil.safeRelease(one); + ReferenceCountUtil.safeRelease(two); + ReferenceCountUtil.safeRelease(three); + } + + @Override + public String toString(Charset charset) { + StringBuilder builder = new StringBuilder(3); + builder.append(one.toString(charset)); + builder.append(two.toString(charset)); + builder.append(three.toString(charset)); + return builder.toString(); + } + + @Override + public String toString(int index, int length, Charset charset) { + // TODO - make this smarter + return toString(charset).substring(index, length); + } + + @Override + public String toString() { + return "Tuple3ByteBuf{" + + "capacity=" + + capacity + + ", one=" + + one + + ", two=" + + two + + ", three=" + + three + + ", allocator=" + + allocator + + ", oneReadIndex=" + + oneReadIndex + + ", twoReadIndex=" + + twoReadIndex + + ", threeReadIndex=" + + threeReadIndex + + ", oneReadableBytes=" + + oneReadableBytes + + ", twoReadableBytes=" + + twoReadableBytes + + ", threeReadableBytes=" + + threeReadableBytes + + ", twoRelativeIndex=" + + twoRelativeIndex + + ", threeRelativeIndex=" + + threeRelativeIndex + + '}'; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuffs.java b/rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuffs.java new file mode 100644 index 000000000..854bfea14 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuffs.java @@ -0,0 +1,35 @@ +package io.rsocket.buffer; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import java.util.Objects; + +public abstract class TupleByteBuffs { + + private TupleByteBuffs() {} + + public static ByteBuf of(ByteBuf one, ByteBuf two) { + return of(ByteBufAllocator.DEFAULT, one, two); + } + + public static ByteBuf of(ByteBufAllocator allocator, ByteBuf one, ByteBuf two) { + Objects.requireNonNull(allocator); + Objects.requireNonNull(one); + Objects.requireNonNull(two); + + return new Tuple2ByteBuf(allocator, one, two); + } + + public static ByteBuf of(ByteBuf one, ByteBuf two, ByteBuf three) { + return of(ByteBufAllocator.DEFAULT, one, two, three); + } + + public static ByteBuf of(ByteBufAllocator allocator, ByteBuf one, ByteBuf two, ByteBuf three) { + Objects.requireNonNull(allocator); + Objects.requireNonNull(one); + Objects.requireNonNull(two); + Objects.requireNonNull(three); + + return new Tuple3ByteBuf(allocator, one, two, three); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java index 6a493fffe..df4024749 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java @@ -3,6 +3,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.rsocket.buffer.TupleByteBuffs; class DataAndMetadataFlyweight { public static final int FRAME_LENGTH_MASK = 0xFFFFFF; @@ -32,11 +33,11 @@ private static int decodeLength(final ByteBuf byteBuf) { static ByteBuf encodeOnlyMetadata( ByteBufAllocator allocator, final ByteBuf header, ByteBuf metadata) { - return allocator.compositeBuffer(2).addComponents(true, header, metadata); + return TupleByteBuffs.of(allocator, header, metadata); } static ByteBuf encodeOnlyData(ByteBufAllocator allocator, final ByteBuf header, ByteBuf data) { - return allocator.compositeBuffer(2).addComponents(true, header, data); + return TupleByteBuffs.of(allocator, header, data); } static ByteBuf encode( @@ -45,7 +46,7 @@ static ByteBuf encode( int length = metadata.readableBytes(); encodeLength(header, length); - return allocator.compositeBuffer(3).addComponents(true, header, metadata, data); + return TupleByteBuffs.of(allocator, header, metadata, data); } static ByteBuf metadataWithoutMarking(ByteBuf byteBuf, boolean hasMetadata) { @@ -59,6 +60,7 @@ static ByteBuf metadataWithoutMarking(ByteBuf byteBuf, boolean hasMetadata) { static ByteBuf metadata(ByteBuf byteBuf, boolean hasMetadata) { byteBuf.markReaderIndex(); + byteBuf.skipBytes(6); ByteBuf metadata = metadataWithoutMarking(byteBuf, hasMetadata); byteBuf.resetReaderIndex(); return metadata; @@ -71,7 +73,7 @@ static ByteBuf dataWithoutMarking(ByteBuf byteBuf, boolean hasMetadata) { byteBuf.skipBytes(length); } if (byteBuf.readableBytes() > 0) { - return byteBuf.slice(); + return byteBuf.readSlice(byteBuf.readableBytes()); } else { return Unpooled.EMPTY_BUFFER; } @@ -79,6 +81,7 @@ static ByteBuf dataWithoutMarking(ByteBuf byteBuf, boolean hasMetadata) { static ByteBuf data(ByteBuf byteBuf, boolean hasMetadata) { byteBuf.markReaderIndex(); + byteBuf.skipBytes(6); ByteBuf data = dataWithoutMarking(byteBuf, hasMetadata); byteBuf.resetReaderIndex(); return data; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameFlyweight.java index 18516eb27..df8b308e9 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameFlyweight.java @@ -22,7 +22,9 @@ public static ByteBuf encode( ByteBuf header = FrameHeaderFlyweight.encode(allocator, streamId, FrameType.EXT, flags); header.writeInt(extendedType); - if (metadata != null) { + if (data == null && metadata == null) { + return header; + } else if (metadata != null) { return DataAndMetadataFlyweight.encode(allocator, header, metadata, data); } else { return DataAndMetadataFlyweight.encodeOnlyData(allocator, header, data); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java index d5b3742b5..06efeab6c 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java @@ -12,10 +12,13 @@ public static ByteBuf encode(final ByteBufAllocator allocator, ByteBuf header, B public static ByteBuf encode( final ByteBufAllocator allocator, ByteBuf header, @Nullable ByteBuf metadata, ByteBuf data) { - if (metadata == null) { - return DataAndMetadataFlyweight.encodeOnlyData(allocator, header, data); - } else { + + if (data == null && metadata == null) { + return header; + } else if (metadata != null) { return DataAndMetadataFlyweight.encode(allocator, header, metadata, data); + } else { + return DataAndMetadataFlyweight.encodeOnlyData(allocator, header, data); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java index 622160061..950f715a2 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java @@ -2,6 +2,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.rsocket.buffer.TupleByteBuffs; /** * Some transports like TCP aren't framed, and require a length. This is used by DuplexConnections @@ -34,7 +35,7 @@ private static int decodeLength(final ByteBuf byteBuf) { public static ByteBuf encode(ByteBufAllocator allocator, int length, ByteBuf frame) { ByteBuf buffer = allocator.buffer(); encodeLength(buffer, length); - return allocator.compositeBuffer(2).addComponents(true, buffer, frame); + return TupleByteBuffs.of(allocator, buffer, frame); } public static int length(ByteBuf byteBuf) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFlyweight.java index 8747da0bb..527cdfafb 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFlyweight.java @@ -24,7 +24,11 @@ public static ByteBuf encode( .writeInt(ttl) .writeInt(numRequests); - return DataAndMetadataFlyweight.encodeOnlyMetadata(allocator, header, metadata); + if (metadata == null) { + return header; + } else { + return DataAndMetadataFlyweight.encodeOnlyMetadata(allocator, header, metadata); + } } public static int ttl(final ByteBuf byteBuf) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestFlyweight.java index 8196c56d8..98d862f36 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestFlyweight.java @@ -53,7 +53,9 @@ ByteBuf encode( header.writeInt(requestN); } - if (metadata != null) { + if (data == null && metadata == null) { + return header; + } else if (metadata != null) { return DataAndMetadataFlyweight.encode(allocator, header, metadata, data); } else { return DataAndMetadataFlyweight.encodeOnlyData(allocator, header, data); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java index 746485067..440b9e178 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java @@ -90,7 +90,9 @@ public static ByteBuf encode( length = ByteBufUtil.utf8Bytes(dataMimeType); header.writeByte(length); ByteBufUtil.writeUtf8(header, dataMimeType); - if (metadata != null) { + if (data == null && metadata == null) { + return header; + } else if (metadata != null) { return DataAndMetadataFlyweight.encode(allocator, header, metadata, data); } else { return DataAndMetadataFlyweight.encodeOnlyData(allocator, header, data); @@ -171,8 +173,8 @@ public static ByteBuf resumeToken(ByteBuf byteBuf) { public static String metadataMimeType(ByteBuf byteBuf) { int skip = bytesToSkipToMimeType(byteBuf); byteBuf.markReaderIndex(); - int length = byteBuf.skipBytes(skip).readByte(); - String mimeType = byteBuf.readSlice(length).toString(StandardCharsets.UTF_8); + int length = byteBuf.skipBytes(skip).readUnsignedByte(); + String mimeType = byteBuf.slice(byteBuf.readerIndex(), length).toString(StandardCharsets.UTF_8); byteBuf.resetReaderIndex(); return mimeType; } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java index df68da9a5..984207936 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java @@ -31,6 +31,8 @@ void fragmentAndReassembleData() { PayloadFrameFlyweight.encodeNextComplete(allocator, 2, DefaultPayload.create(data)); System.out.println(FrameUtil.toString(frame)); + frame.retain(); + Publisher fragments = FrameFragmenter.fragmentFrame( allocator, 64, frame, FrameHeaderFlyweight.frameType(frame), false); diff --git a/rsocket-core/src/test/java/io/rsocket/frame/DataAndMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/DataAndMetadataFlyweightTest.java new file mode 100644 index 000000000..6f9113d73 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/DataAndMetadataFlyweightTest.java @@ -0,0 +1,51 @@ +package io.rsocket.frame; + +import io.netty.buffer.*; +import org.junit.jupiter.api.Test; + +class DataAndMetadataFlyweightTest { + @Test + void testEncodeData() { + ByteBuf header = FrameHeaderFlyweight.encode(ByteBufAllocator.DEFAULT, 1, FrameType.PAYLOAD, 0); + ByteBuf data = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "_I'm data_"); + ByteBuf frame = DataAndMetadataFlyweight.encodeOnlyData(ByteBufAllocator.DEFAULT, header, data); + ByteBuf d = DataAndMetadataFlyweight.data(frame, false); + String s = ByteBufUtil.prettyHexDump(d); + System.out.println(s); + } + + @Test + void testEncodeMetadata() { + ByteBuf header = FrameHeaderFlyweight.encode(ByteBufAllocator.DEFAULT, 1, FrameType.PAYLOAD, 0); + ByteBuf data = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "_I'm metadata_"); + ByteBuf frame = + DataAndMetadataFlyweight.encodeOnlyMetadata(ByteBufAllocator.DEFAULT, header, data); + ByteBuf d = DataAndMetadataFlyweight.data(frame, false); + String s = ByteBufUtil.prettyHexDump(d); + System.out.println(s); + } + + @Test + void testEncodeDataAndMetadata() { + ByteBuf header = + FrameHeaderFlyweight.encode(ByteBufAllocator.DEFAULT, 1, FrameType.REQUEST_RESPONSE, 0); + ByteBuf data = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "_I'm data_"); + ByteBuf metadata = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "_I'm metadata_"); + ByteBuf frame = + DataAndMetadataFlyweight.encode(ByteBufAllocator.DEFAULT, header, metadata, data); + ByteBuf m = DataAndMetadataFlyweight.metadata(frame, true); + String s = ByteBufUtil.prettyHexDump(m); + System.out.println(s); + FrameType frameType = FrameHeaderFlyweight.frameType(frame); + System.out.println(frameType); + + for (int i = 0; i < 10_000_000; i++) { + ByteBuf d1 = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "_I'm data_"); + ByteBuf m1 = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "_I'm metadata_"); + ByteBuf h1 = + FrameHeaderFlyweight.encode(ByteBufAllocator.DEFAULT, 1, FrameType.REQUEST_RESPONSE, 0); + ByteBuf f1 = DataAndMetadataFlyweight.encode(ByteBufAllocator.DEFAULT, h1, m1, d1); + f1.release(); + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/internal/SwitchTransformFluxTest.java b/rsocket-core/src/test/java/io/rsocket/internal/SwitchTransformFluxTest.java index 2297d6bfa..07fbf695f 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/SwitchTransformFluxTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/SwitchTransformFluxTest.java @@ -11,6 +11,7 @@ import java.util.concurrent.atomic.AtomicLong; import org.junit.Assert; import org.junit.Assume; +import org.junit.Ignore; import org.junit.Test; import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; @@ -20,6 +21,7 @@ import reactor.test.util.RaceTestUtils; import reactor.util.context.Context; +@Ignore public class SwitchTransformFluxTest { @Test diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java index 879df959b..306be4e43 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java @@ -18,6 +18,7 @@ import io.rsocket.RSocket; import io.rsocket.RSocketFactory; +import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.test.PingClient; import io.rsocket.transport.netty.client.WebsocketClientTransport; import java.time.Duration; @@ -28,7 +29,10 @@ public final class WebsocketPing { public static void main(String... args) { Mono client = - RSocketFactory.connect().transport(WebsocketClientTransport.create(7878)).start(); + RSocketFactory.connect() + .frameDecoder(PayloadDecoder.ZERO_COPY) + .transport(WebsocketClientTransport.create(7878)) + .start(); PingClient pingClient = new PingClient(client); diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPongServer.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPongServer.java index c94a8c539..7fdb1813a 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPongServer.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPongServer.java @@ -17,6 +17,7 @@ package io.rsocket.transport.netty; import io.rsocket.RSocketFactory; +import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.test.PingHandler; import io.rsocket.transport.netty.server.WebsocketServerTransport; @@ -24,6 +25,7 @@ public final class WebsocketPongServer { public static void main(String... args) { RSocketFactory.receive() + .frameDecoder(PayloadDecoder.ZERO_COPY) .acceptor(new PingHandler()) .transport(WebsocketServerTransport.create(7878)) .start() From 7c87f6c0c9dacc2f45f507d657337406f1f45022 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Tue, 16 Apr 2019 00:55:34 +0300 Subject: [PATCH 025/181] fix issue when long running channels eventually hang under load Signed-off-by: Maksym Ostroverkhov --- .../transport/netty/SendPublisher.java | 59 +++++++++++-------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java index beeec93b0..a697524b3 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java @@ -2,7 +2,6 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; -import io.netty.channel.ChannelPromise; import io.netty.channel.EventLoop; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; @@ -85,26 +84,22 @@ class SendPublisher extends Flux { fuse = queue instanceof Fuseable.QueueSubscription; } - private ChannelPromise writeCleanupPromise(V poll) { - return channel - .newPromise() - .addListener( - future -> { - if (requested != Long.MAX_VALUE) { - requested--; - } - requestedUpstream--; - pending--; + @SuppressWarnings("unchecked") + private void writeCleanup(V poll) { + if (requested != Long.MAX_VALUE) { + requested--; + } + requestedUpstream--; + pending--; - InnerSubscriber is = (InnerSubscriber) INNER_SUBSCRIBER.get(SendPublisher.this); - if (is != null) { - is.tryRequestMoreUpstream(); - tryComplete(is); - } - if (poll.refCnt() > 0) { - ReferenceCountUtil.safeRelease(poll); - } - }); + InnerSubscriber is = (InnerSubscriber) INNER_SUBSCRIBER.get(SendPublisher.this); + if (is != null) { + is.tryRequestMoreUpstream(); + tryComplete(is); + } + if (poll.refCnt() > 0) { + ReferenceCountUtil.safeRelease(poll); + } } private void tryComplete(InnerSubscriber is) { @@ -207,7 +202,7 @@ private void flush() { } private void tryDrain() { - if (wip == 0 && terminated == 0 && WIP.getAndIncrement(SendPublisher.this) == 0) { + if (terminated == 0 && WIP.getAndIncrement(SendPublisher.this) == 0) { try { if (eventLoop.inEventLoop()) { drain(); @@ -235,11 +230,29 @@ private void drain() { int readableBytes = sizeOf.size(poll); pending++; if (channel.isWritable() && readableBytes <= channel.bytesBeforeUnwritable()) { - channel.write(poll, writeCleanupPromise(poll)); + channel + .write(poll) + .addListener( + future -> { + if (future.cause() != null) { + onError(future.cause()); + } else { + writeCleanup(poll); + } + }); scheduleFlush = true; } else { scheduleFlush = false; - channel.writeAndFlush(poll, writeCleanupPromise(poll)); + channel + .writeAndFlush(poll) + .addListener( + future -> { + if (future.cause() != null) { + onError(future.cause()); + } else { + writeCleanup(poll); + } + }); } tryRequestMoreUpstream(); From d835b14a50114c1a742db53da0ae112ce7549b44 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Tue, 16 Apr 2019 11:33:45 -0700 Subject: [PATCH 026/181] update version to 0.12.1-RC4 Signed-off-by: Robert Roeser --- gradle.properties | 3 +-- .../buffer/{TupleByteBuffs.java => TupleByteBuf.java} | 4 ++-- .../java/io/rsocket/frame/DataAndMetadataFlyweight.java | 8 ++++---- .../main/java/io/rsocket/frame/FrameLengthFlyweight.java | 4 ++-- 4 files changed, 9 insertions(+), 10 deletions(-) rename rsocket-core/src/main/java/io/rsocket/buffer/{TupleByteBuffs.java => TupleByteBuf.java} (93%) diff --git a/gradle.properties b/gradle.properties index 6a4960955..205372fc5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # - -version=0.12.1-RC4-SNAPSHOT +version=0.12.1-RC4 diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuffs.java b/rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuf.java similarity index 93% rename from rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuffs.java rename to rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuf.java index 854bfea14..8c8e2e7e4 100644 --- a/rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuffs.java +++ b/rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuf.java @@ -4,9 +4,9 @@ import io.netty.buffer.ByteBufAllocator; import java.util.Objects; -public abstract class TupleByteBuffs { +public abstract class TupleByteBuf { - private TupleByteBuffs() {} + private TupleByteBuf() {} public static ByteBuf of(ByteBuf one, ByteBuf two) { return of(ByteBufAllocator.DEFAULT, one, two); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java index df4024749..8eb571d5c 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java @@ -3,7 +3,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; -import io.rsocket.buffer.TupleByteBuffs; +import io.rsocket.buffer.TupleByteBuf; class DataAndMetadataFlyweight { public static final int FRAME_LENGTH_MASK = 0xFFFFFF; @@ -33,11 +33,11 @@ private static int decodeLength(final ByteBuf byteBuf) { static ByteBuf encodeOnlyMetadata( ByteBufAllocator allocator, final ByteBuf header, ByteBuf metadata) { - return TupleByteBuffs.of(allocator, header, metadata); + return TupleByteBuf.of(allocator, header, metadata); } static ByteBuf encodeOnlyData(ByteBufAllocator allocator, final ByteBuf header, ByteBuf data) { - return TupleByteBuffs.of(allocator, header, data); + return TupleByteBuf.of(allocator, header, data); } static ByteBuf encode( @@ -46,7 +46,7 @@ static ByteBuf encode( int length = metadata.readableBytes(); encodeLength(header, length); - return TupleByteBuffs.of(allocator, header, metadata, data); + return TupleByteBuf.of(allocator, header, metadata, data); } static ByteBuf metadataWithoutMarking(ByteBuf byteBuf, boolean hasMetadata) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java index 950f715a2..6011263fa 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java @@ -2,7 +2,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.rsocket.buffer.TupleByteBuffs; +import io.rsocket.buffer.TupleByteBuf; /** * Some transports like TCP aren't framed, and require a length. This is used by DuplexConnections @@ -35,7 +35,7 @@ private static int decodeLength(final ByteBuf byteBuf) { public static ByteBuf encode(ByteBufAllocator allocator, int length, ByteBuf frame) { ByteBuf buffer = allocator.buffer(); encodeLength(buffer, length); - return TupleByteBuffs.of(allocator, buffer, frame); + return TupleByteBuf.of(allocator, buffer, frame); } public static int length(ByteBuf byteBuf) { From 10bf27c1ab2f510d9fad5c22a0605e5da33314d4 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Tue, 16 Apr 2019 11:48:22 -0700 Subject: [PATCH 027/181] set version to snapshot Signed-off-by: Robert Roeser --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 205372fc5..bd9da4d3f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.12.1-RC4 +version=0.12.1-RC5-SNAPSHOT From 35ddded1b9d9cd460c8993b3210a2f7b382ce096 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Tue, 16 Apr 2019 11:55:24 -0700 Subject: [PATCH 028/181] update version to 0.12.2-RC1-SNAPSHOT Signed-off-by: Robert Roeser --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index bd9da4d3f..c727f0a6d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.12.1-RC5-SNAPSHOT +version=0.12.2-RC1-SNAPSHOT From bd287dec179e5b7359819ce253a4a3e46deb2d30 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Tue, 16 Apr 2019 13:44:18 -0700 Subject: [PATCH 029/181] remove addon Signed-off-by: Robert Roeser --- .travis.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2743ae0bc..8e0789f83 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,12 +29,6 @@ env: script: ci/travis.sh -addons: - apt: - packages: - - oracle-java8-installer - # - oracle-java9-installer - before_cache: - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ From eb5e43e10185b844eeb530d02612a8e58758ed8c Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Tue, 16 Apr 2019 13:56:43 -0700 Subject: [PATCH 030/181] fix travis jdk build and update to 0.12.2-RC1 Signed-off-by: Robert Roeser --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c727f0a6d..3de093819 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.12.2-RC1-SNAPSHOT +version=0.12.2-RC1 From fcdbb55bcb4fe9edf35f6f95c150d8c5c32a1d6a Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Tue, 16 Apr 2019 13:58:00 -0700 Subject: [PATCH 031/181] update version Signed-off-by: Robert Roeser --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 3de093819..55c39f119 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.12.2-RC1 +version=0.12.2-RC2-SNAPSHOT From 3de56c5b78164afb449e9e0571e2da06736c890f Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Thu, 18 Apr 2019 02:41:08 -0700 Subject: [PATCH 032/181] fixing error with framing where it (#620) Signed-off-by: Robert Roeser --- .../java/io/rsocket/frame/DataAndMetadataFlyweight.java | 8 +++----- .../main/java/io/rsocket/frame/FrameLengthFlyweight.java | 3 +-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java index 8eb571d5c..c1366b6b8 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java @@ -3,7 +3,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; -import io.rsocket.buffer.TupleByteBuf; class DataAndMetadataFlyweight { public static final int FRAME_LENGTH_MASK = 0xFFFFFF; @@ -33,11 +32,11 @@ private static int decodeLength(final ByteBuf byteBuf) { static ByteBuf encodeOnlyMetadata( ByteBufAllocator allocator, final ByteBuf header, ByteBuf metadata) { - return TupleByteBuf.of(allocator, header, metadata); + return allocator.compositeBuffer().addComponents(true, header, metadata); } static ByteBuf encodeOnlyData(ByteBufAllocator allocator, final ByteBuf header, ByteBuf data) { - return TupleByteBuf.of(allocator, header, data); + return allocator.compositeBuffer().addComponents(true, header, data); } static ByteBuf encode( @@ -45,8 +44,7 @@ static ByteBuf encode( int length = metadata.readableBytes(); encodeLength(header, length); - - return TupleByteBuf.of(allocator, header, metadata, data); + return allocator.compositeBuffer().addComponents(true, header, metadata, data); } static ByteBuf metadataWithoutMarking(ByteBuf byteBuf, boolean hasMetadata) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java index 6011263fa..63633ed45 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java @@ -2,7 +2,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.rsocket.buffer.TupleByteBuf; /** * Some transports like TCP aren't framed, and require a length. This is used by DuplexConnections @@ -35,7 +34,7 @@ private static int decodeLength(final ByteBuf byteBuf) { public static ByteBuf encode(ByteBufAllocator allocator, int length, ByteBuf frame) { ByteBuf buffer = allocator.buffer(); encodeLength(buffer, length); - return TupleByteBuf.of(allocator, buffer, frame); + return allocator.compositeBuffer().addComponents(true, buffer, frame); } public static int length(ByteBuf byteBuf) { From 36948445cc30e580207a3477f39705a45cc02cee Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Thu, 18 Apr 2019 02:48:43 -0700 Subject: [PATCH 033/181] update version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 55c39f119..f5b674ddf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.12.2-RC2-SNAPSHOT +version=0.12.2-RC2 From 3bbd093c3fa87f94fef60ba790e2a891864583a2 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Thu, 18 Apr 2019 02:51:11 -0700 Subject: [PATCH 034/181] update version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index f5b674ddf..d5b97cda1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.12.2-RC2 +version=0.12.2-RC3-SNAPSHOT From 6405898e82e8a05004ef8ce83197e1ba1c6bc225 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 18 Apr 2019 22:07:17 +0300 Subject: [PATCH 035/181] keep-alives inside rsockets to restore rsocket -> netty-transport fusion (#621) Signed-off-by: Maksym Ostroverkhov --- .../main/java/io/rsocket/RSocketClient.java | 55 +++- .../main/java/io/rsocket/RSocketFactory.java | 58 ++-- .../main/java/io/rsocket/RSocketServer.java | 57 +++- .../internal/ClientServerConnection.java | 39 --- .../ClientServerInputMultiplexer.java | 8 +- .../java/io/rsocket/internal/ClientSetup.java | 60 ++-- .../io/rsocket/internal/KeepAliveData.java | 41 --- .../java/io/rsocket/internal/ServerSetup.java | 64 +--- .../keepalive/KeepAliveConnection.java | 169 ---------- .../keepalive/KeepAliveFramesAcceptor.java | 10 + .../rsocket/keepalive/KeepAliveHandler.java | 177 ++--------- .../rsocket/keepalive/KeepAliveSupport.java | 179 +++++++++++ .../rsocket/resume/ClientRSocketSession.java | 16 +- .../resume/ClientResumeConfiguration.java | 54 ---- .../io/rsocket/resume/RSocketSession.java | 6 +- .../resume/ResumableDuplexConnection.java | 43 ++- .../resume/ResumePositionsConnection.java | 24 -- .../io/rsocket/resume/ResumptionState.java | 67 ---- .../rsocket/resume/ServerRSocketSession.java | 28 +- .../resume/ServerResumeConfiguration.java | 48 --- .../test/java/io/rsocket/KeepAliveTest.java | 296 ++++++++---------- 21 files changed, 576 insertions(+), 923 deletions(-) delete mode 100644 rsocket-core/src/main/java/io/rsocket/internal/ClientServerConnection.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/internal/KeepAliveData.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveConnection.java create mode 100644 rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveFramesAcceptor.java create mode 100644 rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ClientResumeConfiguration.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumePositionsConnection.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ResumptionState.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/resume/ServerResumeConfiguration.java diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java index 9218135de..0c97c05de 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java @@ -16,16 +16,23 @@ package io.rsocket; +import static io.rsocket.keepalive.KeepAliveSupport.ClientKeepAliveSupport; +import static io.rsocket.keepalive.KeepAliveSupport.KeepAlive; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.ReferenceCountUtil; import io.netty.util.collection.IntObjectHashMap; +import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.exceptions.Exceptions; import io.rsocket.frame.*; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.LimitableRequestPublisher; import io.rsocket.internal.UnboundedProcessor; import io.rsocket.internal.UnicastMonoProcessor; +import io.rsocket.keepalive.KeepAliveFramesAcceptor; +import io.rsocket.keepalive.KeepAliveHandler; +import io.rsocket.keepalive.KeepAliveSupport; import java.nio.channels.ClosedChannelException; import java.util.Collections; import java.util.Map; @@ -36,11 +43,7 @@ import org.reactivestreams.Processor; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; -import reactor.core.publisher.BaseSubscriber; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.SignalType; -import reactor.core.publisher.UnicastProcessor; +import reactor.core.publisher.*; /** Client Side of a RSocket socket. Sends {@link ByteBuf}s to a {@link RSocketServer} */ class RSocketClient implements RSocket { @@ -54,6 +57,7 @@ class RSocketClient implements RSocket { private final UnboundedProcessor sendProcessor; private final Lifecycle lifecycle = new Lifecycle(); private final ByteBufAllocator allocator; + private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; /*client requester*/ RSocketClient( @@ -61,7 +65,10 @@ class RSocketClient implements RSocket { DuplexConnection connection, PayloadDecoder payloadDecoder, Consumer errorConsumer, - StreamIdSupplier streamIdSupplier) { + StreamIdSupplier streamIdSupplier, + int keepAliveTickPeriod, + int keepAliveAckTimeout, + KeepAliveHandler keepAliveHandler) { this.allocator = allocator; this.connection = connection; this.payloadDecoder = payloadDecoder; @@ -87,6 +94,34 @@ class RSocketClient implements RSocket { .subscribe(null, this::handleSendProcessorError); connection.receive().subscribe(this::handleIncomingFrames, errorConsumer); + + if (keepAliveTickPeriod != 0 && keepAliveHandler != null) { + KeepAliveSupport keepAliveSupport = + new ClientKeepAliveSupport(allocator, keepAliveTickPeriod, keepAliveAckTimeout); + this.keepAliveFramesAcceptor = + keepAliveHandler.start(keepAliveSupport, sendProcessor::onNext, this::terminate); + } else { + keepAliveFramesAcceptor = null; + } + } + + /*server requester*/ + RSocketClient( + ByteBufAllocator allocator, + DuplexConnection connection, + PayloadDecoder payloadDecoder, + Consumer errorConsumer, + StreamIdSupplier streamIdSupplier) { + this(allocator, connection, payloadDecoder, errorConsumer, streamIdSupplier, 0, 0, null); + } + + private void terminate(KeepAlive keepAlive) { + String message = + String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()); + ConnectionErrorException err = new ConnectionErrorException(message); + lifecycle.setTerminationError(err); + errorConsumer.accept(err); + connection.dispose(); } private void handleSendProcessorError(Throwable t) { @@ -398,6 +433,9 @@ private boolean contains(int streamId) { protected void terminate() { lifecycle.setTerminationError(new ClosedChannelException()); + if (keepAliveFramesAcceptor != null) { + keepAliveFramesAcceptor.dispose(); + } try { receivers.values().forEach(this::cleanUpSubscriber); senders.values().forEach(this::cleanUpLimitableRequestPublisher); @@ -452,8 +490,9 @@ private void handleStreamZero(FrameType type, ByteBuf frame) { case LEASE: break; case KEEPALIVE: - // KeepAlive is handled by corresponding connection interceptor, - // just release its frame here + if (keepAliveFramesAcceptor != null) { + keepAliveFramesAcceptor.receive(frame); + } break; default: // Ignore unknown frames. Throwing an error will close the socket. diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 2560ac01a..807e1e0a9 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -16,6 +16,9 @@ package io.rsocket; +import static io.rsocket.internal.ClientSetup.DefaultClientSetup; +import static io.rsocket.internal.ClientSetup.ResumableClientSetup; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.exceptions.InvalidSetupException; @@ -26,9 +29,8 @@ import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.ClientServerInputMultiplexer; import io.rsocket.internal.ClientSetup; -import io.rsocket.internal.KeepAliveData; import io.rsocket.internal.ServerSetup; -import io.rsocket.keepalive.KeepAliveConnection; +import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.plugins.DuplexConnectionInterceptor; import io.rsocket.plugins.PluginRegistry; import io.rsocket.plugins.Plugins; @@ -272,9 +274,10 @@ public Mono start() { return newConnection() .flatMap( connection -> { - ClientSetup clientSetup = clientSetup(); - DuplexConnection wrappedConnection = clientSetup.wrappedConnection(connection); + ClientSetup clientSetup = clientSetup(connection); ByteBuf resumeToken = clientSetup.resumeToken(); + KeepAliveHandler keepAliveHandler = clientSetup.keepAliveHandler(); + DuplexConnection wrappedConnection = clientSetup.connection(); ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(wrappedConnection, plugins); @@ -285,7 +288,10 @@ public Mono start() { multiplexer.asClientConnection(), payloadDecoder, errorConsumer, - StreamIdSupplier.clientSupplier()); + StreamIdSupplier.clientSupplier(), + keepAliveTickPeriod(), + keepAliveTimeout(), + keepAliveHandler); RSocket wrappedRSocketClient = plugins.applyClient(rSocketClient); @@ -305,8 +311,8 @@ public Mono start() { SetupFrameFlyweight.encode( allocator, false, - (int) keepAliveTickPeriod(), - (int) keepAliveTimeout(), + keepAliveTickPeriod(), + keepAliveTimeout(), resumeToken, metadataMimeType, dataMimeType, @@ -317,19 +323,20 @@ public Mono start() { }); } - private long keepAliveTickPeriod() { - return tickPeriod.toMillis(); + private int keepAliveTickPeriod() { + return (int) tickPeriod.toMillis(); } - private long keepAliveTimeout() { - return ackTimeout.toMillis() + tickPeriod.toMillis() * missedAcks; + private int keepAliveTimeout() { + return (int) (ackTimeout.toMillis() + tickPeriod.toMillis() * missedAcks); } - private ClientSetup clientSetup() { + private ClientSetup clientSetup(DuplexConnection startConnection) { if (resumeEnabled) { ByteBuf resumeToken = resumeTokenSupplier.get(); - return new ClientSetup.ResumableClientSetup( + return new ResumableClientSetup( allocator, + startConnection, newConnection(), resumeToken, resumeStoreFactory.apply(resumeToken), @@ -338,21 +345,12 @@ private ClientSetup clientSetup() { resumeStrategySupplier, resumeCleanupStoreOnKeepAlive); } else { - return new ClientSetup.DefaultClientSetup(); + return new DefaultClientSetup(startConnection); } } - private Mono newConnection() { - return transportClient - .get() - .connect(mtu) - .map( - connection -> - KeepAliveConnection.ofClient( - allocator, - connection, - notUsed -> new KeepAliveData(keepAliveTickPeriod(), keepAliveTimeout()), - errorConsumer)); + private Mono newConnection() { + return transportClient.get().connect(mtu); } } } @@ -464,9 +462,6 @@ public Start transport(Supplier> tra } private Mono acceptor(ServerSetup serverSetup, DuplexConnection connection) { - connection = - KeepAliveConnection.ofServer( - allocator, connection, serverSetup::keepAliveData, errorConsumer); ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(connection, plugins); @@ -512,7 +507,7 @@ private Mono acceptSetup( return serverSetup.acceptRSocketSetup( setupFrame, multiplexer, - wrappedMultiplexer -> { + (keepAliveHandler, wrappedMultiplexer) -> { ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(setupFrame); RSocketClient rSocketClient = @@ -539,7 +534,10 @@ private Mono acceptSetup( wrappedMultiplexer.asClientConnection(), wrappedRSocketServer, payloadDecoder, - errorConsumer); + errorConsumer, + setupPayload.keepAliveInterval(), + setupPayload.keepAliveMaxLifetime(), + keepAliveHandler); }) .doFinally(signalType -> setupPayload.release()) .then(); diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java index 5aabb0ffa..6b1ed4c00 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java @@ -16,15 +16,22 @@ package io.rsocket; +import static io.rsocket.keepalive.KeepAliveSupport.KeepAlive; +import static io.rsocket.keepalive.KeepAliveSupport.ServerKeepAliveSupport; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.ReferenceCountUtil; import io.netty.util.collection.IntObjectHashMap; import io.rsocket.exceptions.ApplicationErrorException; +import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.frame.*; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.LimitableRequestPublisher; import io.rsocket.internal.UnboundedProcessor; +import io.rsocket.keepalive.KeepAliveFramesAcceptor; +import io.rsocket.keepalive.KeepAliveHandler; +import io.rsocket.keepalive.KeepAliveSupport; import java.util.Collections; import java.util.Map; import java.util.function.Consumer; @@ -34,11 +41,7 @@ import org.reactivestreams.Subscription; import reactor.core.Disposable; import reactor.core.Exceptions; -import reactor.core.publisher.BaseSubscriber; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.SignalType; -import reactor.core.publisher.UnicastProcessor; +import reactor.core.publisher.*; /** Server side RSocket. Receives {@link ByteBuf}s from a {@link RSocketClient} */ class RSocketServer implements ResponderRSocket { @@ -55,14 +58,28 @@ class RSocketServer implements ResponderRSocket { private final UnboundedProcessor sendProcessor; private final ByteBufAllocator allocator; + private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; - /*server responder*/ + /*client responder*/ RSocketServer( ByteBufAllocator allocator, DuplexConnection connection, RSocket requestHandler, PayloadDecoder payloadDecoder, Consumer errorConsumer) { + this(allocator, connection, requestHandler, payloadDecoder, errorConsumer, 0, 0, null); + } + + /*server responder*/ + RSocketServer( + ByteBufAllocator allocator, + DuplexConnection connection, + RSocket requestHandler, + PayloadDecoder payloadDecoder, + Consumer errorConsumer, + int keepAliveTickPeriod, + int keepAliveAckTimeout, + KeepAliveHandler keepAliveHandler) { this.allocator = allocator; this.connection = connection; @@ -101,6 +118,22 @@ class RSocketServer implements ResponderRSocket { receiveDisposable.dispose(); }) .subscribe(null, errorConsumer); + + if (keepAliveTickPeriod != 0 && keepAliveHandler != null) { + KeepAliveSupport keepAliveSupport = + new ServerKeepAliveSupport(allocator, keepAliveTickPeriod, keepAliveAckTimeout); + keepAliveFramesAcceptor = + keepAliveHandler.start(keepAliveSupport, sendProcessor::onNext, this::terminate); + } else { + keepAliveFramesAcceptor = null; + } + } + + private void terminate(KeepAlive keepAlive) { + String message = + String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()); + errorConsumer.accept(new ConnectionErrorException(message)); + connection.dispose(); } private void handleSendProcessorError(Throwable t) { @@ -247,6 +280,9 @@ public Mono onClose() { } private void cleanup() { + if (keepAliveFramesAcceptor != null) { + keepAliveFramesAcceptor.dispose(); + } cleanUpSendingSubscriptions(); cleanUpChannelProcessors(); @@ -283,8 +319,7 @@ private void handleFrame(ByteBuf frame) { handleCancelFrame(streamId); break; case KEEPALIVE: - // KeepAlive is handled by corresponding connection interceptor, - // just release its frame here + handleKeepAliveFrame(frame); break; case REQUEST_N: handleRequestN(streamId, frame); @@ -493,6 +528,12 @@ private void handleChannel(int streamId, Payload payload, int initialRequestN) { } } + private void handleKeepAliveFrame(ByteBuf frame) { + if (keepAliveFramesAcceptor != null) { + keepAliveFramesAcceptor.receive(frame); + } + } + private void handleCancelFrame(int streamId) { Subscription subscription = sendingSubscriptions.remove(streamId); diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerConnection.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerConnection.java deleted file mode 100644 index 1b89a6edd..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerConnection.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright 2015-2019 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.internal; - -import io.rsocket.DuplexConnection; -import io.rsocket.resume.ResumePositionsConnection; -import io.rsocket.resume.ResumeStateHolder; -import io.rsocket.util.DuplexConnectionProxy; - -class ClientServerConnection extends DuplexConnectionProxy implements ResumePositionsConnection { - - private final DuplexConnection resumeAware; - - public ClientServerConnection(DuplexConnection delegate, DuplexConnection resumeAware) { - super(delegate); - this.resumeAware = resumeAware; - } - - @Override - public void acceptResumeState(ResumeStateHolder resumeStateHolder) { - if (resumeAware instanceof ResumePositionsConnection) { - ((ResumePositionsConnection) resumeAware).acceptResumeState(resumeStateHolder); - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index d1a83cec5..e69c6631c 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -23,7 +23,6 @@ import io.rsocket.frame.FrameUtil; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; import io.rsocket.plugins.PluginRegistry; -import io.rsocket.resume.ResumePositionsConnection; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +51,7 @@ public class ClientServerInputMultiplexer implements Closeable { private final DuplexConnection serverConnection; private final DuplexConnection clientConnection; private final DuplexConnection source; - private final ResumePositionsConnection clientServerConnection; + private final DuplexConnection clientServerConnection; public ClientServerInputMultiplexer(DuplexConnection source) { this(source, emptyPluginRegistry); @@ -71,8 +70,7 @@ public ClientServerInputMultiplexer(DuplexConnection source, PluginRegistry plug plugins.applyConnection(Type.SERVER, new InternalDuplexConnection(source, server)); clientConnection = plugins.applyConnection(Type.CLIENT, new InternalDuplexConnection(source, client)); - clientServerConnection = - new ClientServerConnection(new InternalDuplexConnection(source, client, server), source); + clientServerConnection = new InternalDuplexConnection(source, client, server); source .receive() @@ -115,7 +113,7 @@ public ClientServerInputMultiplexer(DuplexConnection source, PluginRegistry plug }); } - public ResumePositionsConnection asClientServerConnection() { + public DuplexConnection asClientServerConnection() { return clientServerConnection; } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java index 5f92488b1..5f7248cbe 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java @@ -16,12 +16,15 @@ package io.rsocket.internal; +import static io.rsocket.keepalive.KeepAliveHandler.*; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.rsocket.DuplexConnection; -import io.rsocket.keepalive.KeepAliveConnection; +import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.resume.ClientRSocketSession; +import io.rsocket.resume.ResumableDuplexConnection; import io.rsocket.resume.ResumableFramesStore; import io.rsocket.resume.ResumeStrategy; import java.time.Duration; @@ -29,19 +32,30 @@ import reactor.core.publisher.Mono; public interface ClientSetup { - /*Provide different connections for SETUP / RESUME cases*/ - DuplexConnection wrappedConnection(KeepAliveConnection duplexConnection); - /*Provide different resume tokens for SETUP / RESUME cases*/ + DuplexConnection connection(); + + KeepAliveHandler keepAliveHandler(); + ByteBuf resumeToken(); class DefaultClientSetup implements ClientSetup { + private final DuplexConnection connection; + + public DefaultClientSetup(DuplexConnection connection) { + this.connection = connection; + } @Override - public DuplexConnection wrappedConnection(KeepAliveConnection connection) { + public DuplexConnection connection() { return connection; } + @Override + public KeepAliveHandler keepAliveHandler() { + return new DefaultKeepAliveHandler(); + } + @Override public ByteBuf resumeToken() { return Unpooled.EMPTY_BUFFER; @@ -50,35 +64,20 @@ public ByteBuf resumeToken() { class ResumableClientSetup implements ClientSetup { private final ByteBuf resumeToken; - private final ByteBufAllocator allocator; - private final Mono newConnectionFactory; - private final Duration resumeSessionDuration; - private final Supplier resumeStrategySupplier; - private final ResumableFramesStore resumableFramesStore; - private final Duration resumeStreamTimeout; - private final boolean cleanupStoreOnKeepAlive; + private final ResumableDuplexConnection duplexConnection; + private final ResumableKeepAliveHandler keepAliveHandler; public ResumableClientSetup( ByteBufAllocator allocator, - Mono newConnectionFactory, + DuplexConnection connection, + Mono newConnectionFactory, ByteBuf resumeToken, ResumableFramesStore resumableFramesStore, Duration resumeSessionDuration, Duration resumeStreamTimeout, Supplier resumeStrategySupplier, boolean cleanupStoreOnKeepAlive) { - this.allocator = allocator; - this.newConnectionFactory = newConnectionFactory; - this.resumeToken = resumeToken; - this.resumeSessionDuration = resumeSessionDuration; - this.resumeStrategySupplier = resumeStrategySupplier; - this.resumableFramesStore = resumableFramesStore; - this.resumeStreamTimeout = resumeStreamTimeout; - this.cleanupStoreOnKeepAlive = cleanupStoreOnKeepAlive; - } - @Override - public DuplexConnection wrappedConnection(KeepAliveConnection connection) { ClientRSocketSession rSocketSession = new ClientRSocketSession( connection, @@ -90,8 +89,19 @@ public DuplexConnection wrappedConnection(KeepAliveConnection connection) { cleanupStoreOnKeepAlive) .continueWith(newConnectionFactory) .resumeToken(resumeToken); + this.duplexConnection = rSocketSession.resumableConnection(); + this.keepAliveHandler = new ResumableKeepAliveHandler(duplexConnection); + this.resumeToken = resumeToken; + } - return rSocketSession.resumableConnection(); + @Override + public DuplexConnection connection() { + return duplexConnection; + } + + @Override + public KeepAliveHandler keepAliveHandler() { + return keepAliveHandler; } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/internal/KeepAliveData.java b/rsocket-core/src/main/java/io/rsocket/internal/KeepAliveData.java deleted file mode 100644 index 25338df31..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/KeepAliveData.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2015-2019 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.internal; - -import java.time.Duration; - -public class KeepAliveData { - private final Duration tickPeriod; - private final Duration timeout; - - public KeepAliveData(long tickPeriodMillis, long timeoutMillis) { - this(Duration.ofMillis(tickPeriodMillis), Duration.ofMillis(timeoutMillis)); - } - - public KeepAliveData(Duration tickPeriod, Duration timeout) { - this.tickPeriod = tickPeriod; - this.timeout = timeout; - } - - public Duration getTickPeriod() { - return tickPeriod; - } - - public Duration getTimeout() { - return timeout; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java b/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java index 17e9e53cb..2868fd9f0 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java @@ -16,36 +16,31 @@ package io.rsocket.internal; +import static io.rsocket.keepalive.KeepAliveHandler.*; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.rsocket.DuplexConnection; import io.rsocket.exceptions.RejectedResumeException; import io.rsocket.exceptions.UnsupportedSetupException; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.FrameType; import io.rsocket.frame.ResumeFrameFlyweight; import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.resume.*; import io.rsocket.util.ConnectionUtils; import java.time.Duration; +import java.util.function.BiFunction; import java.util.function.Function; -import javax.annotation.Nullable; import reactor.core.publisher.Mono; public interface ServerSetup { - /*accept connection as SETUP*/ + Mono acceptRSocketSetup( ByteBuf frame, ClientServerInputMultiplexer multiplexer, - Function> then); + BiFunction> then); - /*accept connection as RESUME*/ Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer); - /*get KEEP-ALIVE timings based on start frame: SETUP (directly) /RESUME (lookup by resume token)*/ - @Nullable - KeepAliveData keepAliveData(ByteBuf frame); - default void dispose() {} class DefaultServerSetup implements ServerSetup { @@ -59,7 +54,7 @@ public DefaultServerSetup(ByteBufAllocator allocator) { public Mono acceptRSocketSetup( ByteBuf frame, ClientServerInputMultiplexer multiplexer, - Function> then) { + BiFunction> then) { if (SetupFrameFlyweight.resumeEnabled(frame)) { return sendError(multiplexer, new UnsupportedSetupException("resume not supported")) @@ -69,7 +64,7 @@ public Mono acceptRSocketSetup( multiplexer.dispose(); }); } else { - return then.apply(multiplexer); + return then.apply(new DefaultKeepAliveHandler(), multiplexer); } } @@ -84,17 +79,6 @@ public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexe }); } - @Override - public KeepAliveData keepAliveData(ByteBuf frame) { - if (FrameHeaderFlyweight.frameType(frame) == FrameType.SETUP) { - return new KeepAliveData( - SetupFrameFlyweight.keepAliveInterval(frame), - SetupFrameFlyweight.keepAliveMaxLifetime(frame)); - } else { - return null; - } - } - private Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { return ConnectionUtils.sendError(allocator, multiplexer, exception); } @@ -127,17 +111,12 @@ public ResumableServerSetup( public Mono acceptRSocketSetup( ByteBuf frame, ClientServerInputMultiplexer multiplexer, - Function> then) { + BiFunction> then) { if (SetupFrameFlyweight.resumeEnabled(frame)) { ByteBuf resumeToken = SetupFrameFlyweight.resumeToken(frame); - KeepAliveData keepAliveData = - new KeepAliveData( - SetupFrameFlyweight.keepAliveInterval(frame), - SetupFrameFlyweight.keepAliveMaxLifetime(frame)); - - DuplexConnection resumableConnection = + ResumableDuplexConnection connection = sessionManager .save( new ServerRSocketSession( @@ -147,12 +126,13 @@ public Mono acceptRSocketSetup( resumeStreamTimeout, resumeStoreFactory, resumeToken, - keepAliveData, cleanupStoreOnKeepAlive)) .resumableConnection(); - return then.apply(new ClientServerInputMultiplexer(resumableConnection)); + return then.apply( + new ResumableKeepAliveHandler(connection), + new ClientServerInputMultiplexer(connection)); } else { - return then.apply(multiplexer); + return then.apply(new DefaultKeepAliveHandler(), multiplexer); } } @@ -175,22 +155,6 @@ public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexe } } - @Override - public KeepAliveData keepAliveData(ByteBuf frame) { - if (FrameHeaderFlyweight.frameType(frame) == FrameType.SETUP) { - return new KeepAliveData( - SetupFrameFlyweight.keepAliveInterval(frame), - SetupFrameFlyweight.keepAliveMaxLifetime(frame)); - } else { - ServerRSocketSession session = sessionManager.get(ResumeFrameFlyweight.token(frame)); - if (session != null) { - return session.keepAliveData(); - } else { - return null; - } - } - } - private Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { return ConnectionUtils.sendError(allocator, multiplexer, exception); } diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveConnection.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveConnection.java deleted file mode 100644 index 738b13b02..000000000 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveConnection.java +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Copyright 2015-2019 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.keepalive; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.rsocket.DuplexConnection; -import io.rsocket.exceptions.ConnectionErrorException; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.FrameType; -import io.rsocket.internal.KeepAliveData; -import io.rsocket.resume.ResumePositionsConnection; -import io.rsocket.resume.ResumeStateHolder; -import io.rsocket.util.DuplexConnectionProxy; -import io.rsocket.util.Function3; -import java.time.Duration; -import java.util.function.Consumer; -import java.util.function.Function; -import javax.annotation.Nullable; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; - -public class KeepAliveConnection extends DuplexConnectionProxy - implements ResumePositionsConnection { - - private final MonoProcessor keepAliveHandlerReady = MonoProcessor.create(); - private final ByteBufAllocator allocator; - private final Function keepAliveData; - private final Function3 - keepAliveHandlerFactory; - private final Consumer errorConsumer; - private volatile KeepAliveHandler keepAliveHandler; - private volatile ResumeStateHolder resumeStateHolder; - private volatile boolean keepAliveHandlerStarted; - - public static KeepAliveConnection ofClient( - ByteBufAllocator allocator, - DuplexConnection duplexConnection, - Function keepAliveData, - Consumer errorConsumer) { - - return new KeepAliveConnection( - allocator, duplexConnection, keepAliveData, KeepAliveHandler::ofClient, errorConsumer); - } - - public static KeepAliveConnection ofServer( - ByteBufAllocator allocator, - DuplexConnection duplexConnection, - Function keepAliveData, - Consumer errorConsumer) { - - return new KeepAliveConnection( - allocator, duplexConnection, keepAliveData, KeepAliveHandler::ofServer, errorConsumer); - } - - private KeepAliveConnection( - ByteBufAllocator allocator, - DuplexConnection duplexConnection, - Function keepAliveData, - Function3 keepAliveHandlerFactory, - Consumer errorConsumer) { - super(duplexConnection); - this.allocator = allocator; - this.keepAliveData = keepAliveData; - this.keepAliveHandlerFactory = keepAliveHandlerFactory; - this.errorConsumer = errorConsumer; - keepAliveHandlerReady.subscribe(this::startKeepAlives); - } - - private void startKeepAlives(KeepAliveHandler keepAliveHandler) { - this.keepAliveHandler = keepAliveHandler; - - send(keepAliveHandler.send()).subscribe(null, err -> keepAliveHandler.dispose()); - - keepAliveHandler - .timeout() - .subscribe( - keepAlive -> { - String message = - String.format("No keep-alive acks for %d ms", keepAlive.getTimeoutMillis()); - ConnectionErrorException err = new ConnectionErrorException(message); - errorConsumer.accept(err); - dispose(); - }); - keepAliveHandler.start(); - } - - @Override - public Mono send(Publisher frames) { - return super.send(Flux.from(frames).doOnNext(this::startKeepAliveHandlerOnce)); - } - - @Override - public Flux receive() { - return super.receive() - .doOnNext( - f -> { - if (isKeepAliveFrame(f)) { - long receivedPos = keepAliveHandler.receive(f); - if (receivedPos > 0) { - ResumeStateHolder h = this.resumeStateHolder; - if (h != null) { - h.onImpliedPosition(receivedPos); - } - } - } else { - startKeepAliveHandlerOnce(f); - } - }); - } - - @Override - public Mono onClose() { - return super.onClose() - .doFinally( - s -> { - KeepAliveHandler keepAliveHandler = keepAliveHandlerReady.peek(); - if (keepAliveHandler != null) { - keepAliveHandler.dispose(); - } - }); - } - - @Override - public void acceptResumeState(ResumeStateHolder resumeStateHolder) { - this.resumeStateHolder = resumeStateHolder; - keepAliveHandlerReady.subscribe(h -> h.resumeState(resumeStateHolder)); - } - - private void startKeepAliveHandlerOnce(ByteBuf f) { - if (!keepAliveHandlerStarted && isStartFrame(f)) { - keepAliveHandlerStarted = true; - startKeepAliveHandler(keepAliveData.apply(f)); - } - } - - private static boolean isStartFrame(ByteBuf frame) { - FrameType frameType = FrameHeaderFlyweight.frameType(frame); - return frameType == FrameType.SETUP || frameType == FrameType.RESUME; - } - - private static boolean isKeepAliveFrame(ByteBuf frame) { - return FrameHeaderFlyweight.frameType(frame) == FrameType.KEEPALIVE; - } - - private void startKeepAliveHandler(@Nullable KeepAliveData kad) { - if (kad != null) { - KeepAliveHandler handler = - keepAliveHandlerFactory.apply(allocator, kad.getTickPeriod(), kad.getTimeout()); - keepAliveHandlerReady.onNext(handler); - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveFramesAcceptor.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveFramesAcceptor.java new file mode 100644 index 000000000..db6314e71 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveFramesAcceptor.java @@ -0,0 +1,10 @@ +package io.rsocket.keepalive; + +import io.netty.buffer.ByteBuf; + +public interface KeepAliveFramesAcceptor { + + void receive(ByteBuf keepAliveFrame); + + void dispose(); +} diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java index 7a07d2ea5..cc1f53c32 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java @@ -1,164 +1,49 @@ -/* - * Copyright 2015-2019 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.keepalive; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.rsocket.frame.KeepAliveFrameFlyweight; -import io.rsocket.resume.ResumeStateHolder; -import java.time.Duration; -import java.util.concurrent.atomic.AtomicReference; -import reactor.core.Disposable; -import reactor.core.Disposables; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; -import reactor.core.publisher.UnicastProcessor; - -abstract class KeepAliveHandler implements Disposable { - final ByteBufAllocator allocator; - private final Duration keepAlivePeriod; - private final long keepAliveTimeout; - private volatile ResumeStateHolder resumeStateHolder; - private final UnicastProcessor sent = UnicastProcessor.create(); - private final MonoProcessor timeout = MonoProcessor.create(); - private final AtomicReference intervalDisposable = new AtomicReference<>(); - private volatile long lastReceivedMillis; - - static KeepAliveHandler ofServer( - ByteBufAllocator allocator, Duration keepAlivePeriod, Duration keepAliveTimeout) { - return new KeepAliveHandler.Server(allocator, keepAlivePeriod, keepAliveTimeout); - } - - static KeepAliveHandler ofClient( - ByteBufAllocator allocator, Duration keepAlivePeriod, Duration keepAliveTimeout) { - return new KeepAliveHandler.Client(allocator, keepAlivePeriod, keepAliveTimeout); - } - - private KeepAliveHandler( - ByteBufAllocator allocator, Duration keepAlivePeriod, Duration keepAliveTimeout) { - this.allocator = allocator; - this.keepAlivePeriod = keepAlivePeriod; - this.keepAliveTimeout = keepAliveTimeout.toMillis(); - } - - public void start() { - this.lastReceivedMillis = System.currentTimeMillis(); - intervalDisposable.compareAndSet( - null, Flux.interval(keepAlivePeriod).subscribe(v -> onIntervalTick())); - } - - @Override - public void dispose() { - Disposable d = intervalDisposable.getAndSet(Disposables.disposed()); - if (d != null) { - d.dispose(); - } - sent.onComplete(); - timeout.onComplete(); - } - - public long receive(ByteBuf keepAliveFrame) { - this.lastReceivedMillis = System.currentTimeMillis(); - long remoteLastReceivedPos = KeepAliveFrameFlyweight.lastPosition(keepAliveFrame); - if (KeepAliveFrameFlyweight.respondFlag(keepAliveFrame)) { - long localLastReceivedPos = obtainLastReceivedPos(); - doSend( - KeepAliveFrameFlyweight.encode( - allocator, - false, - localLastReceivedPos, - KeepAliveFrameFlyweight.data(keepAliveFrame).retain())); - } - return remoteLastReceivedPos; - } - - public void resumeState(ResumeStateHolder resumeStateHolder) { - this.resumeStateHolder = resumeStateHolder; - } - - public Flux send() { - return sent; - } - - public Mono timeout() { - return timeout; - } - - abstract void onIntervalTick(); - - void doSend(ByteBuf frame) { - sent.onNext(frame); - } +import io.rsocket.keepalive.KeepAliveSupport.KeepAlive; +import io.rsocket.resume.ResumableDuplexConnection; +import java.util.function.Consumer; - void doCheckTimeout() { - long now = System.currentTimeMillis(); - if (now - lastReceivedMillis >= keepAliveTimeout) { - timeout.onNext(new KeepAlive(keepAlivePeriod.toMillis(), keepAliveTimeout)); - } - } +public interface KeepAliveHandler { - long obtainLastReceivedPos() { - return resumeStateHolder != null ? resumeStateHolder.impliedPosition() : 0; - } + KeepAliveFramesAcceptor start( + KeepAliveSupport keepAliveSupport, + Consumer onFrameSent, + Consumer onTimeout); - private static class Server extends KeepAliveHandler { - - Server(ByteBufAllocator allocator, Duration keepAlivePeriod, Duration keepAliveTimeout) { - super(allocator, keepAlivePeriod, keepAliveTimeout); - } + class DefaultKeepAliveHandler implements KeepAliveHandler { @Override - void onIntervalTick() { - doCheckTimeout(); + public KeepAliveFramesAcceptor start( + KeepAliveSupport keepAliveSupport, + Consumer onSendKeepAliveFrame, + Consumer onTimeout) { + return keepAliveSupport + .onSendKeepAliveFrame(onSendKeepAliveFrame) + .onTimeout(onTimeout) + .start(); } } - private static final class Client extends KeepAliveHandler { + class ResumableKeepAliveHandler implements KeepAliveHandler { + private final ResumableDuplexConnection resumableDuplexConnection; - Client(ByteBufAllocator allocator, Duration keepAlivePeriod, Duration keepAliveTimeout) { - super(allocator, keepAlivePeriod, keepAliveTimeout); + public ResumableKeepAliveHandler(ResumableDuplexConnection resumableDuplexConnection) { + this.resumableDuplexConnection = resumableDuplexConnection; } @Override - void onIntervalTick() { - doCheckTimeout(); - doSend( - KeepAliveFrameFlyweight.encode( - allocator, true, obtainLastReceivedPos(), Unpooled.EMPTY_BUFFER)); - } - } - - public static final class KeepAlive { - private final long tickPeriod; - private final long timeoutMillis; - - public KeepAlive(long tickPeriod, long timeoutMillis) { - this.tickPeriod = tickPeriod; - this.timeoutMillis = timeoutMillis; - } - - public long getTickPeriod() { - return tickPeriod; - } - - public long getTimeoutMillis() { - return timeoutMillis; + public KeepAliveFramesAcceptor start( + KeepAliveSupport keepAliveSupport, + Consumer onSendKeepAliveFrame, + Consumer onTimeout) { + resumableDuplexConnection.onResumed(keepAliveSupport::start); + return keepAliveSupport + .resumeState(resumableDuplexConnection) + .onSendKeepAliveFrame(onSendKeepAliveFrame) + .onTimeout(keepAlive -> resumableDuplexConnection.disconnect()) + .start(); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java new file mode 100644 index 000000000..9573b70e6 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java @@ -0,0 +1,179 @@ +/* + * Copyright 2015-2019 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.keepalive; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.frame.KeepAliveFrameFlyweight; +import io.rsocket.resume.ResumeStateHolder; +import java.time.Duration; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; + +public abstract class KeepAliveSupport implements Disposable, KeepAliveFramesAcceptor { + final ByteBufAllocator allocator; + private final Duration keepAliveInterval; + private final Duration keepAliveTimeout; + private final long keepAliveTimeoutMillis; + private volatile Consumer onTimeout; + private volatile Consumer onFrameSent; + private volatile Disposable ticksDisposable; + private final AtomicBoolean started = new AtomicBoolean(); + + private volatile ResumeStateHolder resumeStateHolder; + private volatile long lastReceivedMillis; + + private KeepAliveSupport( + ByteBufAllocator allocator, int keepAliveInterval, int keepAliveTimeout) { + this.allocator = allocator; + this.keepAliveInterval = Duration.ofMillis(keepAliveInterval); + this.keepAliveTimeout = Duration.ofMillis(keepAliveTimeout); + this.keepAliveTimeoutMillis = keepAliveTimeout; + } + + public KeepAliveSupport start() { + this.lastReceivedMillis = System.currentTimeMillis(); + startTicks(); + return this; + } + + @Override + public void dispose() { + stopTicks(); + } + + @Override + public void receive(ByteBuf keepAliveFrame) { + this.lastReceivedMillis = System.currentTimeMillis(); + if (resumeStateHolder != null) { + long remoteLastReceivedPos = remoteLastReceivedPosition(keepAliveFrame); + resumeStateHolder.onImpliedPosition(remoteLastReceivedPos); + } + if (KeepAliveFrameFlyweight.respondFlag(keepAliveFrame)) { + long localLastReceivedPos = localLastReceivedPosition(); + send( + KeepAliveFrameFlyweight.encode( + allocator, + false, + localLastReceivedPos, + KeepAliveFrameFlyweight.data(keepAliveFrame).retain())); + } + } + + public KeepAliveSupport resumeState(ResumeStateHolder resumeStateHolder) { + this.resumeStateHolder = resumeStateHolder; + return this; + } + + public KeepAliveSupport onSendKeepAliveFrame(Consumer onFrameSent) { + this.onFrameSent = onFrameSent; + return this; + } + + public KeepAliveSupport onTimeout(Consumer onTimeout) { + this.onTimeout = onTimeout; + return this; + } + + abstract void onIntervalTick(); + + void send(ByteBuf frame) { + if (onFrameSent != null) { + onFrameSent.accept(frame); + } + } + + void tryTimeout() { + long now = System.currentTimeMillis(); + if (now - lastReceivedMillis >= keepAliveTimeoutMillis) { + if (onTimeout != null) { + onTimeout.accept(new KeepAlive(keepAliveInterval, keepAliveTimeout)); + } + stopTicks(); + } + } + + long localLastReceivedPosition() { + return resumeStateHolder != null ? resumeStateHolder.impliedPosition() : 0; + } + + long remoteLastReceivedPosition(ByteBuf keepAliveFrame) { + return KeepAliveFrameFlyweight.lastPosition(keepAliveFrame); + } + + void startTicks() { + if (started.compareAndSet(false, true)) { + ticksDisposable = Flux.interval(keepAliveInterval).subscribe(v -> onIntervalTick()); + } + } + + void stopTicks() { + if (started.compareAndSet(true, false)) { + ticksDisposable.dispose(); + } + } + + public static final class ServerKeepAliveSupport extends KeepAliveSupport { + + public ServerKeepAliveSupport( + ByteBufAllocator allocator, int keepAlivePeriod, int keepAliveTimeout) { + super(allocator, keepAlivePeriod, keepAliveTimeout); + } + + @Override + void onIntervalTick() { + tryTimeout(); + } + } + + public static final class ClientKeepAliveSupport extends KeepAliveSupport { + + public ClientKeepAliveSupport( + ByteBufAllocator allocator, int keepAliveInterval, int keepAliveTimeout) { + super(allocator, keepAliveInterval, keepAliveTimeout); + } + + @Override + void onIntervalTick() { + tryTimeout(); + send( + KeepAliveFrameFlyweight.encode( + allocator, true, localLastReceivedPosition(), Unpooled.EMPTY_BUFFER)); + } + } + + public static final class KeepAlive { + private final Duration tickPeriod; + private final Duration timeoutMillis; + + public KeepAlive(Duration tickPeriod, Duration timeoutMillis) { + this.tickPeriod = tickPeriod; + this.timeoutMillis = timeoutMillis; + } + + public Duration getTickPeriod() { + return tickPeriod; + } + + public Duration getTimeout() { + return timeoutMillis; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java index afe1d4a76..f798a1944 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -31,17 +31,16 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; -public class ClientRSocketSession - implements RSocketSession> { +public class ClientRSocketSession implements RSocketSession> { private static final Logger logger = LoggerFactory.getLogger(ClientRSocketSession.class); private final ResumableDuplexConnection resumableConnection; - private volatile Mono newConnection; + private volatile Mono newConnection; private volatile ByteBuf resumeToken; private final ByteBufAllocator allocator; public ClientRSocketSession( - ResumePositionsConnection duplexConnection, + DuplexConnection duplexConnection, ByteBufAllocator allocator, Duration resumeSessionDuration, Supplier resumeStrategy, @@ -114,9 +113,8 @@ public ClientRSocketSession( } @Override - public ClientRSocketSession continueWith( - Mono newConnection) { - this.newConnection = newConnection; + public ClientRSocketSession continueWith(Mono connectionFactory) { + this.newConnection = connectionFactory; return this; } @@ -151,12 +149,12 @@ public ClientRSocketSession resumeToken(ByteBuf resumeToken) { } @Override - public void reconnect(ResumePositionsConnection connection) { + public void reconnect(DuplexConnection connection) { resumableConnection.reconnect(connection); } @Override - public DuplexConnection resumableConnection() { + public ResumableDuplexConnection resumableConnection() { return resumableConnection; } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientResumeConfiguration.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientResumeConfiguration.java deleted file mode 100644 index 0eb8b20d3..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientResumeConfiguration.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2015-2019 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.resume; - -import java.time.Duration; -import java.util.function.Supplier; - -public class ClientResumeConfiguration { - private final Duration sessionDuration; - private final Supplier resumeStrategy; - private final ResumableFramesStore resumableFramesStore; - private final Duration resumeStreamTimeout; - - public ClientResumeConfiguration( - Duration sessionDuration, - Supplier resumeStrategy, - ResumableFramesStore resumableFramesStore, - Duration resumeStreamTimeout) { - this.sessionDuration = sessionDuration; - this.resumeStrategy = resumeStrategy; - this.resumableFramesStore = resumableFramesStore; - this.resumeStreamTimeout = resumeStreamTimeout; - } - - public Duration sessionDuration() { - return sessionDuration; - } - - public Supplier resumptionStrategy() { - return resumeStrategy; - } - - public ResumableFramesStore resumeStore() { - return resumableFramesStore; - } - - public Duration resumeStreamTimeout() { - return resumeStreamTimeout; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java index b2a1829a9..7ec0abaee 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/RSocketSession.java @@ -25,13 +25,13 @@ public interface RSocketSession extends Closeable { ByteBuf token(); - DuplexConnection resumableConnection(); + ResumableDuplexConnection resumableConnection(); - RSocketSession continueWith(T ResumeAwareConnectionFactory); + RSocketSession continueWith(T ConnectionFactory); RSocketSession resumeWith(ByteBuf resumeFrame); - void reconnect(ResumePositionsConnection connection); + void reconnect(DuplexConnection connection); @Override default Mono onClose() { 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 e46a1927f..dd76001e6 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -34,7 +34,7 @@ import reactor.core.publisher.*; import reactor.util.concurrent.Queues; -class ResumableDuplexConnection implements DuplexConnection, ResumeStateHolder { +public class ResumableDuplexConnection implements DuplexConnection, ResumeStateHolder { private static final Logger logger = LoggerFactory.getLogger(ResumableDuplexConnection.class); private final String tag; @@ -44,13 +44,14 @@ class ResumableDuplexConnection implements DuplexConnection, ResumeStateHolder { private final ReplayProcessor connections = ReplayProcessor.create(1); private final EmitterProcessor connectionErrors = EmitterProcessor.create(); - private volatile ResumePositionsConnection curConnection; + private volatile DuplexConnection curConnection; /*used instead of EmitterProcessor because its autocancel=false capability had no expected effect*/ private final FluxProcessor downStreamFrames = ReplayProcessor.create(0); private final FluxProcessor resumeSaveFrames = EmitterProcessor.create(); private final MonoProcessor resumeSaveCompleted = MonoProcessor.create(); private final Queue actions = Queues.unboundedMultiproducer().get(); private final AtomicInteger actionsWip = new AtomicInteger(); + private final AtomicBoolean disposed = new AtomicBoolean(); private final Mono framesSent; private final RequestListener downStreamRequestListener = new RequestListener(); @@ -63,13 +64,13 @@ class ResumableDuplexConnection implements DuplexConnection, ResumeStateHolder { resumeSaveStreamRequestListener.requests(), this::dispatch); + private volatile Runnable onResumedAction; private volatile int state; private volatile Disposable resumedStreamDisposable = Disposables.disposed(); - private final AtomicBoolean disposed = new AtomicBoolean(); ResumableDuplexConnection( String tag, - ResumePositionsConnection duplexConnection, + DuplexConnection duplexConnection, ResumableFramesStore resumableFramesStore, Duration resumeStreamTimeout, boolean cleanupOnKeepAlive) { @@ -102,14 +103,24 @@ class ResumableDuplexConnection implements DuplexConnection, ResumeStateHolder { reconnect(duplexConnection); } + public void disconnect() { + DuplexConnection c = this.curConnection; + if (c != null) { + disconnect(c); + } + } + + public void onResumed(Runnable onResumedAction) { + this.onResumedAction = onResumedAction; + } + /*reconnected by session after error. After this downstream can receive frames, * but sending in suppressed until resume() is called*/ - public void reconnect(ResumePositionsConnection connection) { + public void reconnect(DuplexConnection connection) { if (curConnection == null) { logger.debug("{} Resumable duplex connection started with connection: {}", tag, connection); state = State.CONNECTED; onNewConnection(connection); - acceptRemoteResumePositions(); } else { logger.debug( "{} Resumable duplex connection reconnected with connection: {}", tag, connection); @@ -199,10 +210,6 @@ public boolean isDisposed() { return disposed.get(); } - private void acceptRemoteResumePositions() { - curConnection.acceptResumeState(this); - } - private void sendFrame(ByteBuf f) { /*resuming from store so no need to save again*/ if (state != State.RESUME && isResumableFrame(f)) { @@ -233,7 +240,7 @@ private void dispatch(Object action) { } } - private void doResumeStart(ResumePositionsConnection connection) { + private void doResumeStart(DuplexConnection connection) { state = State.RESUME_STARTED; resumedStreamDisposable.dispose(); upstreamSubscriber.resumeStart(); @@ -275,6 +282,13 @@ private void doResume( sendResumeFrame .apply(impliedPositionOrError) + .doOnSuccess( + v -> { + Runnable a = this.onResumedAction; + if (a != null) { + a.run(); + } + }) .then( streamResumedFrames( resumableFramesStore @@ -299,7 +313,6 @@ private void doResumeComplete() { logger.debug("Completing resumption"); state = State.RESUME_COMPLETED; upstreamSubscriber.resumeComplete(); - acceptRemoteResumePositions(); } private Mono streamResumedFrames(Flux frames) { @@ -314,7 +327,7 @@ private Mono streamResumedFrames(Flux frames) { }); } - private void onNewConnection(ResumePositionsConnection connection) { + private void onNewConnection(DuplexConnection connection) { curConnection = connection; connection.onClose().doFinally(v -> disconnect(connection)).subscribe(); connections.onNext(connection); @@ -361,9 +374,9 @@ static class State { } class ResumeStart implements Runnable { - private ResumePositionsConnection connection; + private final DuplexConnection connection; - public ResumeStart(ResumePositionsConnection connection) { + public ResumeStart(DuplexConnection connection) { this.connection = connection; } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumePositionsConnection.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumePositionsConnection.java deleted file mode 100644 index 07f865131..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumePositionsConnection.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2015-2019 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.resume; - -import io.rsocket.DuplexConnection; - -public interface ResumePositionsConnection extends DuplexConnection { - - void acceptResumeState(ResumeStateHolder resumeStateHolder); -} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumptionState.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumptionState.java deleted file mode 100644 index 4ca1bcc1f..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumptionState.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2015-2019 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.resume; - -import java.util.Objects; - -class ResumptionState { - private final long pos; - private final long impliedPos; - - ResumptionState(long pos, long impliedPos) { - this.pos = pos; - this.impliedPos = impliedPos; - } - - public static ResumptionState fromServer(long impliedPos) { - return new ResumptionState(-1, impliedPos); - } - - public static ResumptionState fromClient(long pos, long impliedPos) { - return new ResumptionState(pos, impliedPos); - } - - public boolean isServer() { - return pos < 0; - } - - public long position() { - return pos; - } - - public long impliedPosition() { - return impliedPos; - } - - @Override - public String toString() { - return "ResumptionState{" + "pos=" + pos + ", impliedPos=" + impliedPos + '}'; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ResumptionState that = (ResumptionState) o; - return pos == that.pos && impliedPos == that.impliedPos; - } - - @Override - public int hashCode() { - return Objects.hash(pos, impliedPos); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java index d2a42e99b..ed91cc2ab 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -23,7 +23,6 @@ import io.rsocket.frame.ErrorFrameFlyweight; import io.rsocket.frame.ResumeFrameFlyweight; import io.rsocket.frame.ResumeOkFrameFlyweight; -import io.rsocket.internal.KeepAliveData; import java.time.Duration; import java.util.function.Function; import org.slf4j.Logger; @@ -32,28 +31,25 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.ReplayProcessor; -public class ServerRSocketSession implements RSocketSession { +public class ServerRSocketSession implements RSocketSession { private static final Logger logger = LoggerFactory.getLogger(ServerRSocketSession.class); private final ResumableDuplexConnection resumableConnection; /*used instead of EmitterProcessor because its autocancel=false capability had no expected effect*/ - private final FluxProcessor newConnections = + private final FluxProcessor newConnections = ReplayProcessor.create(0); private final ByteBufAllocator allocator; - private final KeepAliveData keepAliveData; private final ByteBuf resumeToken; public ServerRSocketSession( - ResumePositionsConnection duplexConnection, + DuplexConnection duplexConnection, ByteBufAllocator allocator, Duration resumeSessionDuration, Duration resumeStreamTimeout, Function resumeStoreFactory, ByteBuf resumeToken, - KeepAliveData keepAliveData, boolean cleanupStoreOnKeepAlive) { this.allocator = allocator; - this.keepAliveData = keepAliveData; this.resumeToken = resumeToken; this.resumableConnection = new ResumableDuplexConnection( @@ -63,7 +59,7 @@ public ServerRSocketSession( resumeStreamTimeout, cleanupStoreOnKeepAlive); - Mono timeout = + Mono timeout = resumableConnection .connectionErrors() .flatMap( @@ -75,7 +71,7 @@ public ServerRSocketSession( .timeout(resumeSessionDuration); }) .then() - .cast(ResumePositionsConnection.class); + .cast(DuplexConnection.class); newConnections .mergeWith(timeout) @@ -91,9 +87,9 @@ public ServerRSocketSession( } @Override - public ServerRSocketSession continueWith(ResumePositionsConnection newConnection) { - logger.debug("Server continued with connection: {}", newConnection); - newConnections.onNext(newConnection); + public ServerRSocketSession continueWith(DuplexConnection connectionFactory) { + logger.debug("Server continued with connection: {}", connectionFactory); + newConnections.onNext(connectionFactory); return this; } @@ -121,12 +117,12 @@ public ServerRSocketSession resumeWith(ByteBuf resumeFrame) { } @Override - public void reconnect(ResumePositionsConnection connection) { + public void reconnect(DuplexConnection connection) { resumableConnection.reconnect(connection); } @Override - public DuplexConnection resumableConnection() { + public ResumableDuplexConnection resumableConnection() { return resumableConnection; } @@ -135,10 +131,6 @@ public ByteBuf token() { return resumeToken; } - public KeepAliveData keepAliveData() { - return keepAliveData; - } - private Mono sendFrame(ByteBuf frame) { logger.debug("Sending Resume frame: {}", frame); return resumableConnection.sendOne(frame).onErrorResume(e -> Mono.empty()); diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerResumeConfiguration.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerResumeConfiguration.java deleted file mode 100644 index 067db2bb8..000000000 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerResumeConfiguration.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2015-2019 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.resume; - -import io.netty.buffer.ByteBuf; -import java.time.Duration; -import java.util.function.Function; - -public class ServerResumeConfiguration { - private final Duration sessionDuration; - private final Duration resumeStreamTimeout; - private final Function resumeStoreFactory; - - public ServerResumeConfiguration( - Duration sessionDuration, - Duration resumeStreamTimeout, - Function resumeStoreFactory) { - this.sessionDuration = sessionDuration; - this.resumeStreamTimeout = resumeStreamTimeout; - this.resumeStoreFactory = resumeStoreFactory; - } - - public Duration sessionDuration() { - return sessionDuration; - } - - public Duration resumeStreamTimeout() { - return resumeStreamTimeout; - } - - public Function resumeStoreFactory() { - return resumeStoreFactory; - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java index f8708e602..30246c10c 100644 --- a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java @@ -16,221 +16,189 @@ package io.rsocket; +import static io.rsocket.keepalive.KeepAliveHandler.DefaultKeepAliveHandler; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; -import io.netty.util.ReferenceCountUtil; import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; import io.rsocket.frame.KeepAliveFrameFlyweight; -import io.rsocket.frame.SetupFrameFlyweight; -import io.rsocket.internal.KeepAliveData; -import io.rsocket.keepalive.KeepAliveConnection; -import io.rsocket.resume.ResumeStateHolder; import io.rsocket.test.util.TestDuplexConnection; -import java.nio.charset.StandardCharsets; +import io.rsocket.util.DefaultPayload; import java.time.Duration; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.List; import java.util.function.Consumer; -import java.util.function.Function; -import java.util.stream.Collectors; +import java.util.function.Supplier; +import java.util.stream.Stream; import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; public class KeepAliveTest { - private static final int TICK_PERIOD = 100; - private static final int TIMEOUT = 700; - - private TestDuplexConnection testConnection; - private Function timingsProvider; - private List errors; - private Consumer errorConsumer; - private KeepAliveConnection clientConnection; - private KeepAliveConnection serverConnection; - private ByteBufAllocator allocator; - - @BeforeEach - void setUp() { - allocator = ByteBufAllocator.DEFAULT; - testConnection = new TestDuplexConnection(); - - timingsProvider = f -> new KeepAliveData(TICK_PERIOD, TIMEOUT); - errors = new ArrayList<>(); - errorConsumer = errors::add; - - clientConnection = - KeepAliveConnection.ofClient(allocator, testConnection, timingsProvider, errorConsumer); - serverConnection = - KeepAliveConnection.ofServer(allocator, testConnection, timingsProvider, errorConsumer); - } - - @Test - void clientNoFramesBeforeSetup() { - - Mono.delay(Duration.ofSeconds(1)).block(); + private static final int KEEP_ALIVE_INTERVAL = 100; + private static final int KEEP_ALIVE_TIMEOUT = 1000; - Assertions.assertThat(errors).isEmpty(); - Assertions.assertThat(clientConnection.isDisposed()).isFalse(); - Assertions.assertThat(clientConnection.availability()).isEqualTo(testConnection.availability()); - Assertions.assertThat(testConnection.getSent()).isEmpty(); + static Stream> testData() { + return Stream.of( + requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT), + responder(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT)); } - @Test - void clientFramesAfterSetup() { - clientConnection.sendOne(setupFrame()).subscribe(); - - Mono.delay(Duration.ofMillis(500)).block(); - - Assertions.assertThat(errors).isEmpty(); - Assertions.assertThat(clientConnection.isDisposed()).isFalse(); - Collection sent = testConnection.getSent(); - Collection sentAfterSetup = - sent.stream().filter(f -> frameType(f) != FrameType.SETUP).collect(Collectors.toList()); - - Assertions.assertThat(sentAfterSetup).isNotEmpty(); - sentAfterSetup.forEach( - f -> { - Assertions.assertThat(frameType(f)).isEqualTo(FrameType.KEEPALIVE); - Assertions.assertThat(KeepAliveFrameFlyweight.respondFlag(f)).isEqualTo(true); - Assertions.assertThat(KeepAliveFrameFlyweight.lastPosition(f)).isEqualTo(0); - }); - - sent.forEach(ReferenceCountUtil::safeRelease); + static Supplier requester(int tickPeriod, int timeout) { + return () -> { + TestDuplexConnection connection = new TestDuplexConnection(); + Errors errors = new Errors(); + RSocketClient rSocket = + new RSocketClient( + ByteBufAllocator.DEFAULT, + connection, + DefaultPayload::create, + errors, + StreamIdSupplier.clientSupplier(), + tickPeriod, + timeout, + new DefaultKeepAliveHandler()); + return new TestData(rSocket, errors, connection); + }; } - @Test - void clientCloseOnMissingKeepalives() { - clientConnection.sendOne(setupFrame()).subscribe(); - - Mono.delay(Duration.ofSeconds(1)).block(); - - Assertions.assertThat(errors).hasSize(1); - Throwable err = errors.get(0); - Assertions.assertThat(err).isExactlyInstanceOf(ConnectionErrorException.class); - Assertions.assertThat(err.getMessage()).isEqualTo("No keep-alive acks for 700 ms"); - Assertions.assertThat(clientConnection.isDisposed()).isTrue(); - - testConnection.getSent().forEach(ReferenceCountUtil::safeRelease); + static Supplier responder(int tickPeriod, int timeout) { + return () -> { + TestDuplexConnection connection = new TestDuplexConnection(); + AbstractRSocket handler = new AbstractRSocket() {}; + Errors errors = new Errors(); + RSocketServer rSocket = + new RSocketServer( + ByteBufAllocator.DEFAULT, + connection, + handler, + DefaultPayload::create, + errors, + tickPeriod, + timeout, + new DefaultKeepAliveHandler()); + return new TestData(rSocket, errors, connection); + }; } - @Test - void clientResumptionState() { - TestResumeStateHolder resumeStateHolder = new TestResumeStateHolder(); - clientConnection.acceptResumeState(resumeStateHolder); + @ParameterizedTest + @MethodSource("testData") + void rSocketNotDisposedOnPresentKeepAlives(Supplier testDataSupplier) { + TestData testData = testDataSupplier.get(); + TestDuplexConnection connection = testData.connection(); - clientConnection.sendOne(setupFrame()).subscribe(); - clientConnection.receive().subscribe(); + Flux.interval(Duration.ofMillis(100)) + .subscribe( + n -> + connection.addToReceivedBuffer( + KeepAliveFrameFlyweight.encode( + ByteBufAllocator.DEFAULT, true, 0, Unpooled.EMPTY_BUFFER))); - testConnection.addToReceivedBuffer(keepAliveFrame(false, 1)); - testConnection.addToReceivedBuffer(keepAliveFrame(false, 2)); - testConnection.addToReceivedBuffer(keepAliveFrame(false, 3)); + Mono.delay(Duration.ofMillis(1500)).block(); - Mono.delay(Duration.ofMillis(500)).block(); + RSocket rSocket = testData.rSocket(); + List errors = testData.errors().errors(); - List receivedPositions = resumeStateHolder.receivedImpliedPositions(); + Assertions.assertThat(rSocket.isDisposed()).isFalse(); + Assertions.assertThat(errors).isEmpty(); + } - Collection sent = testConnection.getSent(); - List sentPositions = - sent.stream() - .filter(f -> frameType(f) != FrameType.SETUP) - .map(KeepAliveFrameFlyweight::lastPosition) - .limit(4) - .collect(Collectors.toList()); + @ParameterizedTest + @MethodSource("testData") + void rSocketDisposedOnMissingKeepAlives(Supplier testDataSupplier) { + TestData testData = testDataSupplier.get(); + RSocket rSocket = testData.rSocket(); - Assertions.assertThat(sentPositions).isEqualTo(Arrays.asList(1L, 5L, 6L, 8L)); - Assertions.assertThat(receivedPositions).isEqualTo(Arrays.asList(1L, 2L, 3L)); + Mono.delay(Duration.ofMillis(1500)).block(); - sent.forEach(ReferenceCountUtil::safeRelease); + List errors = testData.errors().errors(); + Assertions.assertThat(rSocket.isDisposed()).isTrue(); + Assertions.assertThat(errors).hasSize(1); + Throwable throwable = errors.get(0); + Assertions.assertThat(throwable).isInstanceOf(ConnectionErrorException.class); } @Test - void serverFrames() { - serverConnection.sendOne(setupFrame()).subscribe(); - serverConnection.receive().subscribe(); - testConnection.addToReceivedBuffer(keepAliveFrame(true, 0)); - - Assertions.assertThat(errors).isEmpty(); - Assertions.assertThat(clientConnection.isDisposed()).isFalse(); - - Collection sent = testConnection.getSent(); - Collection sentAfterSetup = - sent.stream().filter(f -> frameType(f) != FrameType.SETUP).collect(Collectors.toList()); - Assertions.assertThat(sentAfterSetup).isNotEmpty(); - - sentAfterSetup.forEach( - f -> { - Assertions.assertThat(frameType(f)).isEqualTo(FrameType.KEEPALIVE); - Assertions.assertThat(KeepAliveFrameFlyweight.respondFlag(f)).isEqualTo(false); - Assertions.assertThat(KeepAliveFrameFlyweight.lastPosition(f)).isEqualTo(0); - }); - - sent.forEach(ReferenceCountUtil::safeRelease); + void clientRequesterSendsKeepAlives() { + TestData testData = requester(100, 1000).get(); + TestDuplexConnection connection = testData.connection(); + + StepVerifier.create(Flux.from(connection.getSentAsPublisher()).take(3)) + .expectNextMatches(this::keepAliveFrameWithRespondFlag) + .expectNextMatches(this::keepAliveFrameWithRespondFlag) + .expectNextMatches(this::keepAliveFrameWithRespondFlag) + .expectComplete() + .verify(Duration.ofSeconds(5)); } @Test - void serverCloseOnMissingKeepalives() { - serverConnection.sendOne(setupFrame()).subscribe(); - - Mono.delay(Duration.ofSeconds(1)).block(); - - Assertions.assertThat(errors).hasSize(1); - Throwable err = errors.get(0); - Assertions.assertThat(err).isExactlyInstanceOf(ConnectionErrorException.class); - Assertions.assertThat(err.getMessage()).isEqualTo("No keep-alive acks for 700 ms"); - Assertions.assertThat(clientConnection.isDisposed()).isTrue(); - - testConnection.getSent().forEach(ReferenceCountUtil::safeRelease); + void serverResponderSendsKeepAlives() { + TestData testData = responder(100, 1000).get(); + TestDuplexConnection connection = testData.connection(); + Mono.delay(Duration.ofMillis(100)) + .subscribe( + l -> + connection.addToReceivedBuffer( + KeepAliveFrameFlyweight.encode( + ByteBufAllocator.DEFAULT, true, 0, Unpooled.EMPTY_BUFFER))); + + StepVerifier.create(Flux.from(connection.getSentAsPublisher()).take(1)) + .expectNextMatches(this::keepAliveFrameWithoutRespondFlag) + .expectComplete() + .verify(Duration.ofSeconds(5)); } - private ByteBuf keepAliveFrame(boolean respond, int pos) { - return KeepAliveFrameFlyweight.encode(allocator, respond, pos, Unpooled.EMPTY_BUFFER); + private boolean keepAliveFrameWithRespondFlag(ByteBuf frame) { + return FrameHeaderFlyweight.frameType(frame) == FrameType.KEEPALIVE + && KeepAliveFrameFlyweight.respondFlag(frame); } - private ByteBuf setupFrame() { - return SetupFrameFlyweight.encode( - allocator, - false, - TICK_PERIOD, - TIMEOUT, - "metadataType", - "dataType", - byteBuf("metadata"), - byteBuf("data")); + private boolean keepAliveFrameWithoutRespondFlag(ByteBuf frame) { + return FrameHeaderFlyweight.frameType(frame) == FrameType.KEEPALIVE + && !KeepAliveFrameFlyweight.respondFlag(frame); } - private ByteBuf byteBuf(String msg) { - return Unpooled.wrappedBuffer(msg.getBytes(StandardCharsets.UTF_8)); - } + static class TestData { + private final RSocket rSocket; + private final Errors errors; + private final TestDuplexConnection connection; - private static FrameType frameType(ByteBuf frame) { - return FrameHeaderFlyweight.frameType(frame); - } + public TestData(RSocket rSocket, Errors errors, TestDuplexConnection connection) { + this.rSocket = rSocket; + this.errors = errors; + this.connection = connection; + } - private static class TestResumeStateHolder implements ResumeStateHolder { - private final List sentPositions = Arrays.asList(1L, 5L, 6L, 8L); - private final List receivedPositions = new ArrayList<>(); - private int counter = 0; + public TestDuplexConnection connection() { + return connection; + } - @Override - public long impliedPosition() { - long res = sentPositions.get(counter); - counter = Math.min(counter + 1, sentPositions.size() - 1); - return res; + public RSocket rSocket() { + return rSocket; } + public Errors errors() { + return errors; + } + } + + static class Errors implements Consumer { + private final List errors = new ArrayList<>(); + @Override - public void onImpliedPosition(long remoteImpliedPos) { - receivedPositions.add(remoteImpliedPos); + public void accept(Throwable throwable) { + errors.add(throwable); } - public List receivedImpliedPositions() { - return receivedPositions; + public List errors() { + return new ArrayList<>(errors); } } } From 1b87e1f0121a468cb290b5b6d6f268b77fa2a4e3 Mon Sep 17 00:00:00 2001 From: Jared Dellitt Date: Thu, 18 Apr 2019 15:02:38 -0500 Subject: [PATCH 036/181] Refresh sockets on `select` in LoadBalancedRSocketMono before checking for active sockets (#623) Signed-off-by: Jared Dellitt --- .../client/LoadBalancedRSocketMono.java | 3 +- .../client/LoadBalancedRSocketMonoTest.java | 78 ++++++++----------- 2 files changed, 33 insertions(+), 48 deletions(-) diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java index 7bea75318..ba799f763 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java @@ -374,10 +374,11 @@ public synchronized double availability() { } private synchronized RSocket select() { + refreshSockets(); + if (activeSockets.isEmpty()) { return FAILING_REACTIVE_SOCKET; } - refreshSockets(); int size = activeSockets.size(); if (size == 1) { diff --git a/rsocket-load-balancer/src/test/java/io/rsocket/client/LoadBalancedRSocketMonoTest.java b/rsocket-load-balancer/src/test/java/io/rsocket/client/LoadBalancedRSocketMonoTest.java index b529e426c..4baa106c5 100644 --- a/rsocket-load-balancer/src/test/java/io/rsocket/client/LoadBalancedRSocketMonoTest.java +++ b/rsocket-load-balancer/src/test/java/io/rsocket/client/LoadBalancedRSocketMonoTest.java @@ -19,19 +19,15 @@ import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.client.filter.RSocketSupplier; -import io.rsocket.util.EmptyPayload; -import java.net.InetSocketAddress; -import java.net.SocketAddress; import java.util.Arrays; +import java.util.Collections; import java.util.List; -import java.util.concurrent.CountDownLatch; +import java.util.concurrent.CompletableFuture; import java.util.function.Function; import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -39,11 +35,8 @@ public class LoadBalancedRSocketMonoTest { @Test(timeout = 10_000L) public void testNeverSelectFailingFactories() throws InterruptedException { - InetSocketAddress local0 = InetSocketAddress.createUnresolved("localhost", 7000); - InetSocketAddress local1 = InetSocketAddress.createUnresolved("localhost", 7001); - TestingRSocket socket = new TestingRSocket(Function.identity()); - RSocketSupplier failing = failingClient(local0); + RSocketSupplier failing = failingClient(); RSocketSupplier succeeding = succeedingFactory(socket); List factories = Arrays.asList(failing, succeeding); @@ -52,9 +45,6 @@ public void testNeverSelectFailingFactories() throws InterruptedException { @Test(timeout = 10_000L) public void testNeverSelectFailingSocket() throws InterruptedException { - InetSocketAddress local0 = InetSocketAddress.createUnresolved("localhost", 7000); - InetSocketAddress local1 = InetSocketAddress.createUnresolved("localhost", 7001); - TestingRSocket socket = new TestingRSocket(Function.identity()); TestingRSocket failingSocket = new TestingRSocket(Function.identity()) { @@ -76,6 +66,33 @@ public double availability() { testBalancer(clients); } + @Test(timeout = 10_000L) + public void testRefreshesSocketsOnSelectBeforeReturningFailedAfterNewFactoriesDelivered() { + TestingRSocket socket = new TestingRSocket(Function.identity()); + + CompletableFuture laterSupplier = new CompletableFuture<>(); + Flux> factories = + Flux.create( + s -> { + s.next(Collections.emptyList()); + + laterSupplier.handle( + (RSocketSupplier result, Throwable t) -> { + s.next(Collections.singletonList(result)); + return null; + }); + }); + + LoadBalancedRSocketMono balancer = LoadBalancedRSocketMono.create(factories); + + Assert.assertEquals(0.0, balancer.availability(), 0); + + laterSupplier.complete(succeedingFactory(socket)); + balancer.rSocketMono.block(); + + Assert.assertEquals(1.0, balancer.availability(), 0); + } + private void testBalancer(List factories) throws InterruptedException { Publisher> src = s -> { @@ -92,39 +109,6 @@ private void testBalancer(List factories) throws InterruptedExc Flux.range(0, 100).flatMap(i -> balancer).blockLast(); } - private void makeAcall(RSocket balancer) throws InterruptedException { - CountDownLatch latch = new CountDownLatch(1); - - balancer - .requestResponse(EmptyPayload.INSTANCE) - .subscribe( - new Subscriber() { - @Override - public void onSubscribe(Subscription s) { - s.request(1L); - } - - @Override - public void onNext(Payload payload) { - System.out.println("Successfully receiving a response"); - } - - @Override - public void onError(Throwable t) { - t.printStackTrace(); - Assert.assertTrue(false); - latch.countDown(); - } - - @Override - public void onComplete() { - latch.countDown(); - } - }); - - latch.await(); - } - private static RSocketSupplier succeedingFactory(RSocket socket) { RSocketSupplier mock = Mockito.mock(RSocketSupplier.class); @@ -135,7 +119,7 @@ private static RSocketSupplier succeedingFactory(RSocket socket) { return mock; } - private static RSocketSupplier failingClient(SocketAddress sa) { + private static RSocketSupplier failingClient() { RSocketSupplier mock = Mockito.mock(RSocketSupplier.class); Mockito.when(mock.availability()).thenReturn(0.0); From 5101748fbd224ec86570351cebd24d079b63fbfc Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Sun, 28 Apr 2019 12:13:24 +0300 Subject: [PATCH 037/181] Resume cleanup round (#624) * add ResumableKeepAlive tests Signed-off-by: Maksym Ostroverkhov * resume in-memory store: switch to Flux.generate Signed-off-by: Maksym Ostroverkhov * Revert "fix issue when long running channels eventually hang under load" This reverts commit 7c87f6c0 Signed-off-by: Maksym Ostroverkhov * fix issue when long running channels eventually hang. Replace https://github.com/rsocket/rsocket-java/pull/618 as It caused memory leak in event of resumption Signed-off-by: Maksym Ostroverkhov --- .../main/java/io/rsocket/RSocketClient.java | 3 - .../main/java/io/rsocket/RSocketServer.java | 3 - .../java/io/rsocket/internal/ClientSetup.java | 2 +- .../java/io/rsocket/internal/ServerSetup.java | 4 +- .../keepalive/KeepAliveFramesAcceptor.java | 2 - .../rsocket/keepalive/KeepAliveHandler.java | 10 +- .../rsocket/keepalive/KeepAliveSupport.java | 27 +-- .../resume/InMemoryResumableFramesStore.java | 52 ++-- .../resume/ResumableDuplexConnection.java | 33 ++- .../test/java/io/rsocket/KeepAliveTest.java | 222 +++++++++++++++--- .../transport/netty/SendPublisher.java | 57 ++--- 11 files changed, 297 insertions(+), 118 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java index 0c97c05de..b4c9dbb20 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java @@ -433,9 +433,6 @@ private boolean contains(int streamId) { protected void terminate() { lifecycle.setTerminationError(new ClosedChannelException()); - if (keepAliveFramesAcceptor != null) { - keepAliveFramesAcceptor.dispose(); - } try { receivers.values().forEach(this::cleanUpSubscriber); senders.values().forEach(this::cleanUpLimitableRequestPublisher); diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java index 6b1ed4c00..451ed7b42 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java @@ -280,9 +280,6 @@ public Mono onClose() { } private void cleanup() { - if (keepAliveFramesAcceptor != null) { - keepAliveFramesAcceptor.dispose(); - } cleanUpSendingSubscriptions(); cleanUpChannelProcessors(); diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java index 5f7248cbe..38217bdc2 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java @@ -53,7 +53,7 @@ public DuplexConnection connection() { @Override public KeepAliveHandler keepAliveHandler() { - return new DefaultKeepAliveHandler(); + return new DefaultKeepAliveHandler(connection); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java b/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java index 2868fd9f0..dbd8bc173 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java @@ -64,7 +64,7 @@ public Mono acceptRSocketSetup( multiplexer.dispose(); }); } else { - return then.apply(new DefaultKeepAliveHandler(), multiplexer); + return then.apply(new DefaultKeepAliveHandler(multiplexer), multiplexer); } } @@ -132,7 +132,7 @@ public Mono acceptRSocketSetup( new ResumableKeepAliveHandler(connection), new ClientServerInputMultiplexer(connection)); } else { - return then.apply(new DefaultKeepAliveHandler(), multiplexer); + return then.apply(new DefaultKeepAliveHandler(multiplexer), multiplexer); } } diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveFramesAcceptor.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveFramesAcceptor.java index db6314e71..6fc96d6d2 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveFramesAcceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveFramesAcceptor.java @@ -5,6 +5,4 @@ public interface KeepAliveFramesAcceptor { void receive(ByteBuf keepAliveFrame); - - void dispose(); } diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java index cc1f53c32..b8ba8d5d0 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java @@ -1,6 +1,7 @@ package io.rsocket.keepalive; import io.netty.buffer.ByteBuf; +import io.rsocket.Closeable; import io.rsocket.keepalive.KeepAliveSupport.KeepAlive; import io.rsocket.resume.ResumableDuplexConnection; import java.util.function.Consumer; @@ -13,12 +14,18 @@ KeepAliveFramesAcceptor start( Consumer onTimeout); class DefaultKeepAliveHandler implements KeepAliveHandler { + private final Closeable duplexConnection; + + public DefaultKeepAliveHandler(Closeable duplexConnection) { + this.duplexConnection = duplexConnection; + } @Override public KeepAliveFramesAcceptor start( KeepAliveSupport keepAliveSupport, Consumer onSendKeepAliveFrame, Consumer onTimeout) { + duplexConnection.onClose().subscribe(v -> keepAliveSupport.stop()); return keepAliveSupport .onSendKeepAliveFrame(onSendKeepAliveFrame) .onTimeout(onTimeout) @@ -38,7 +45,8 @@ public KeepAliveFramesAcceptor start( KeepAliveSupport keepAliveSupport, Consumer onSendKeepAliveFrame, Consumer onTimeout) { - resumableDuplexConnection.onResumed(keepAliveSupport::start); + resumableDuplexConnection.onResume(keepAliveSupport::start); + resumableDuplexConnection.onDisconnect(keepAliveSupport::stop); return keepAliveSupport .resumeState(resumableDuplexConnection) .onSendKeepAliveFrame(onSendKeepAliveFrame) diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java index 9573b70e6..ea8a0de22 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java @@ -27,7 +27,7 @@ import reactor.core.Disposable; import reactor.core.publisher.Flux; -public abstract class KeepAliveSupport implements Disposable, KeepAliveFramesAcceptor { +public abstract class KeepAliveSupport implements KeepAliveFramesAcceptor { final ByteBufAllocator allocator; private final Duration keepAliveInterval; private final Duration keepAliveTimeout; @@ -50,13 +50,16 @@ private KeepAliveSupport( public KeepAliveSupport start() { this.lastReceivedMillis = System.currentTimeMillis(); - startTicks(); + if (started.compareAndSet(false, true)) { + ticksDisposable = Flux.interval(keepAliveInterval).subscribe(v -> onIntervalTick()); + } return this; } - @Override - public void dispose() { - stopTicks(); + public void stop() { + if (started.compareAndSet(true, false)) { + ticksDisposable.dispose(); + } } @Override @@ -106,7 +109,7 @@ void tryTimeout() { if (onTimeout != null) { onTimeout.accept(new KeepAlive(keepAliveInterval, keepAliveTimeout)); } - stopTicks(); + stop(); } } @@ -118,18 +121,6 @@ long remoteLastReceivedPosition(ByteBuf keepAliveFrame) { return KeepAliveFrameFlyweight.lastPosition(keepAliveFrame); } - void startTicks() { - if (started.compareAndSet(false, true)) { - ticksDisposable = Flux.interval(keepAliveInterval).subscribe(v -> onIntervalTick()); - } - } - - void stopTicks() { - if (started.compareAndSet(true, false)) { - ticksDisposable.dispose(); - } - } - public static final class ServerKeepAliveSupport extends KeepAliveSupport { public ServerKeepAliveSupport( diff --git a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java index 8a8692cab..1875b7eac 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/InMemoryResumableFramesStore.java @@ -84,24 +84,22 @@ public void releaseFrames(long remoteImpliedPos) { @Override public Flux resumeStream() { - return Flux.create( - s -> { - int size = cachedFrames.size(); - int refCnt = upstreamFrameRefCnt; - logger.debug("{} Resuming stream size: {}", tag, size); - /*spsc queue has no iterator - iterating by consuming*/ - for (int i = 0; i < size; i++) { + return Flux.generate( + () -> new ResumeStreamState(cachedFrames.size(), upstreamFrameRefCnt), + (state, sink) -> { + if (state.next()) { + /*spsc queue has no iterator - iterating by consuming*/ ByteBuf frame = cachedFrames.poll(); - /*in the event of connection termination some frames - * are not released on DuplexConnection*/ - if (frame.refCnt() == refCnt) { + if (state.shouldRetain(frame)) { frame.retain(); } cachedFrames.offer(frame); - s.next(frame); + sink.next(frame); + } else { + sink.complete(); + logger.debug("{} Resuming stream completed", tag); } - s.complete(); - logger.debug("{} Resuming stream completed", tag); + return state; }); } @@ -177,11 +175,35 @@ void saveFrame(ByteBuf frame) { } } - private static Queue cachedFramesQueue(int size) { + static class ResumeStreamState { + private final int cacheSize; + private final int expectedRefCnt; + private int cacheCounter; + + public ResumeStreamState(int cacheSize, int expectedRefCnt) { + this.cacheSize = cacheSize; + this.expectedRefCnt = expectedRefCnt; + } + + public boolean next() { + if (cacheCounter < cacheSize) { + cacheCounter++; + return true; + } else { + return false; + } + } + + public boolean shouldRetain(ByteBuf frame) { + return frame.refCnt() == expectedRefCnt; + } + } + + static Queue cachedFramesQueue(int size) { return Queues.get(size).get(); } - private class FramesSubscriber implements Subscriber { + class FramesSubscriber implements Subscriber { private final long firstRequestSize; private final long refillSize; private int received; 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 dd76001e6..49401d560 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -36,6 +36,7 @@ public class ResumableDuplexConnection implements DuplexConnection, ResumeStateHolder { private static final Logger logger = LoggerFactory.getLogger(ResumableDuplexConnection.class); + private static final Throwable closedChannelException = new ClosedChannelException(); private final String tag; private final ResumableFramesStore resumableFramesStore; @@ -64,11 +65,12 @@ public class ResumableDuplexConnection implements DuplexConnection, ResumeStateH resumeSaveStreamRequestListener.requests(), this::dispatch); - private volatile Runnable onResumedAction; + private volatile Runnable onResume; + private volatile Runnable onDisconnect; private volatile int state; private volatile Disposable resumedStreamDisposable = Disposables.disposed(); - ResumableDuplexConnection( + public ResumableDuplexConnection( String tag, DuplexConnection duplexConnection, ResumableFramesStore resumableFramesStore, @@ -110,8 +112,12 @@ public void disconnect() { } } - public void onResumed(Runnable onResumedAction) { - this.onResumedAction = onResumedAction; + public void onDisconnect(Runnable onDisconnectAction) { + this.onDisconnect = onDisconnectAction; + } + + public void onResume(Runnable onResumeAction) { + this.onResume = onResumeAction; } /*reconnected by session after error. After this downstream can receive frames, @@ -284,9 +290,9 @@ private void doResume( .apply(impliedPositionOrError) .doOnSuccess( v -> { - Runnable a = this.onResumedAction; - if (a != null) { - a.run(); + Runnable r = this.onResume; + if (r != null) { + r.run(); } }) .then( @@ -336,10 +342,17 @@ private void onNewConnection(DuplexConnection connection) { private void disconnect(DuplexConnection connection) { /*do not report late disconnects on old connection if new one is available*/ if (curConnection == connection && state != State.DISCONNECTED) { - Throwable err = new ClosedChannelException(); + connection.dispose(); state = State.DISCONNECTED; - logger.debug("{} Inner connection disconnected: {}", tag, err.getClass().getSimpleName()); - connectionErrors.onNext(err); + logger.debug( + "{} Inner connection disconnected: {}", + tag, + closedChannelException.getClass().getSimpleName()); + connectionErrors.onNext(closedChannelException); + Runnable r = this.onDisconnect; + if (r != null) { + r.run(); + } } } diff --git a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java index 30246c10c..aededb804 100644 --- a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java @@ -17,6 +17,7 @@ package io.rsocket; import static io.rsocket.keepalive.KeepAliveHandler.DefaultKeepAliveHandler; +import static io.rsocket.keepalive.KeepAliveHandler.ResumableKeepAliveHandler; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; @@ -25,6 +26,8 @@ import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; import io.rsocket.frame.KeepAliveFrameFlyweight; +import io.rsocket.resume.InMemoryResumableFramesStore; +import io.rsocket.resume.ResumableDuplexConnection; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.DefaultPayload; import java.time.Duration; @@ -44,14 +47,21 @@ public class KeepAliveTest { private static final int KEEP_ALIVE_INTERVAL = 100; private static final int KEEP_ALIVE_TIMEOUT = 1000; + private static final int RESUMABLE_KEEP_ALIVE_TIMEOUT = 200; - static Stream> testData() { + static Stream> rSocketStates() { return Stream.of( requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT), responder(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT)); } - static Supplier requester(int tickPeriod, int timeout) { + static Stream> resumableRSocketStates() { + return Stream.of( + resumableRequester(KEEP_ALIVE_INTERVAL, RESUMABLE_KEEP_ALIVE_TIMEOUT), + resumableResponder(KEEP_ALIVE_INTERVAL, RESUMABLE_KEEP_ALIVE_TIMEOUT)); + } + + static Supplier requester(int tickPeriod, int timeout) { return () -> { TestDuplexConnection connection = new TestDuplexConnection(); Errors errors = new Errors(); @@ -64,12 +74,12 @@ static Supplier requester(int tickPeriod, int timeout) { StreamIdSupplier.clientSupplier(), tickPeriod, timeout, - new DefaultKeepAliveHandler()); - return new TestData(rSocket, errors, connection); + new DefaultKeepAliveHandler(connection)); + return new RSocketState(rSocket, errors, connection); }; } - static Supplier responder(int tickPeriod, int timeout) { + static Supplier responder(int tickPeriod, int timeout) { return () -> { TestDuplexConnection connection = new TestDuplexConnection(); AbstractRSocket handler = new AbstractRSocket() {}; @@ -83,16 +93,68 @@ static Supplier responder(int tickPeriod, int timeout) { errors, tickPeriod, timeout, - new DefaultKeepAliveHandler()); - return new TestData(rSocket, errors, connection); + new DefaultKeepAliveHandler(connection)); + return new RSocketState(rSocket, errors, connection); + }; + } + + static Supplier resumableRequester(int tickPeriod, int timeout) { + return () -> { + TestDuplexConnection connection = new TestDuplexConnection(); + ResumableDuplexConnection resumableConnection = + new ResumableDuplexConnection( + "test", + connection, + new InMemoryResumableFramesStore("test", 10_000), + Duration.ofSeconds(10), + false); + + Errors errors = new Errors(); + RSocketClient rSocket = + new RSocketClient( + ByteBufAllocator.DEFAULT, + resumableConnection, + DefaultPayload::create, + errors, + StreamIdSupplier.clientSupplier(), + tickPeriod, + timeout, + new ResumableKeepAliveHandler(resumableConnection)); + return new ResumableRSocketState(rSocket, errors, connection, resumableConnection); + }; + } + + static Supplier resumableResponder(int tickPeriod, int timeout) { + return () -> { + AbstractRSocket handler = new AbstractRSocket() {}; + TestDuplexConnection connection = new TestDuplexConnection(); + ResumableDuplexConnection resumableConnection = + new ResumableDuplexConnection( + "test", + connection, + new InMemoryResumableFramesStore("test", 10_000), + Duration.ofSeconds(10), + false); + Errors errors = new Errors(); + RSocketServer rSocket = + new RSocketServer( + ByteBufAllocator.DEFAULT, + resumableConnection, + handler, + DefaultPayload::create, + errors, + tickPeriod, + timeout, + new ResumableKeepAliveHandler(resumableConnection)); + return new ResumableRSocketState(rSocket, errors, connection, resumableConnection); }; } @ParameterizedTest - @MethodSource("testData") - void rSocketNotDisposedOnPresentKeepAlives(Supplier testDataSupplier) { - TestData testData = testDataSupplier.get(); - TestDuplexConnection connection = testData.connection(); + @MethodSource("rSocketStates") + void rSocketNotDisposedOnPresentKeepAlives(Supplier testDataSupplier) { + RSocketState RSocketState = testDataSupplier.get(); + TestDuplexConnection connection = RSocketState.connection(); Flux.interval(Duration.ofMillis(100)) .subscribe( @@ -103,22 +165,33 @@ void rSocketNotDisposedOnPresentKeepAlives(Supplier testDataSupplier) Mono.delay(Duration.ofMillis(1500)).block(); - RSocket rSocket = testData.rSocket(); - List errors = testData.errors().errors(); + RSocket rSocket = RSocketState.rSocket(); + List errors = RSocketState.errors().errors(); Assertions.assertThat(rSocket.isDisposed()).isFalse(); Assertions.assertThat(errors).isEmpty(); } @ParameterizedTest - @MethodSource("testData") - void rSocketDisposedOnMissingKeepAlives(Supplier testDataSupplier) { - TestData testData = testDataSupplier.get(); - RSocket rSocket = testData.rSocket(); + @MethodSource("rSocketStates") + void noKeepAlivesSentAfterRSocketDispose(Supplier testDataSupplier) { + RSocketState RSocketState = testDataSupplier.get(); + RSocketState.rSocket().dispose(); + StepVerifier.create( + Flux.from(RSocketState.connection().getSentAsPublisher()).take(Duration.ofMillis(500))) + .expectComplete() + .verify(Duration.ofSeconds(1)); + } + + @ParameterizedTest + @MethodSource("rSocketStates") + void rSocketDisposedOnMissingKeepAlives(Supplier testDataSupplier) { + RSocketState rSocketState = testDataSupplier.get(); + RSocket rSocket = rSocketState.rSocket(); Mono.delay(Duration.ofMillis(1500)).block(); - List errors = testData.errors().errors(); + List errors = rSocketState.errors().errors(); Assertions.assertThat(rSocket.isDisposed()).isTrue(); Assertions.assertThat(errors).hasSize(1); Throwable throwable = errors.get(0); @@ -127,8 +200,8 @@ void rSocketDisposedOnMissingKeepAlives(Supplier testDataSupplier) { @Test void clientRequesterSendsKeepAlives() { - TestData testData = requester(100, 1000).get(); - TestDuplexConnection connection = testData.connection(); + RSocketState RSocketState = requester(100, 1000).get(); + TestDuplexConnection connection = RSocketState.connection(); StepVerifier.create(Flux.from(connection.getSentAsPublisher()).take(3)) .expectNextMatches(this::keepAliveFrameWithRespondFlag) @@ -140,8 +213,8 @@ void clientRequesterSendsKeepAlives() { @Test void serverResponderSendsKeepAlives() { - TestData testData = responder(100, 1000).get(); - TestDuplexConnection connection = testData.connection(); + RSocketState RSocketState = responder(100, 1000).get(); + TestDuplexConnection connection = RSocketState.connection(); Mono.delay(Duration.ofMillis(100)) .subscribe( l -> @@ -155,31 +228,124 @@ void serverResponderSendsKeepAlives() { .verify(Duration.ofSeconds(5)); } + @Test + void resumableRequesterNoKeepAlivesAfterDisconnect() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT).get(); + TestDuplexConnection testConnection = rSocketState.connection(); + ResumableDuplexConnection resumableDuplexConnection = rSocketState.resumableDuplexConnection(); + + resumableDuplexConnection.disconnect(); + + StepVerifier.create(Flux.from(testConnection.getSentAsPublisher()).take(Duration.ofMillis(500))) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void resumableRequesterKeepAlivesAfterReconnect() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT).get(); + ResumableDuplexConnection resumableDuplexConnection = rSocketState.resumableDuplexConnection(); + resumableDuplexConnection.disconnect(); + TestDuplexConnection newTestConnection = new TestDuplexConnection(); + resumableDuplexConnection.reconnect(newTestConnection); + resumableDuplexConnection.resume(0, 0, ignored -> Mono.empty()); + + StepVerifier.create(Flux.from(newTestConnection.getSentAsPublisher()).take(1)) + .expectNextMatches(this::keepAliveFrame) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @Test + void resumableRequesterNoKeepAlivesAfterDispose() { + ResumableRSocketState rSocketState = + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT).get(); + rSocketState.rSocket().dispose(); + StepVerifier.create( + Flux.from(rSocketState.connection().getSentAsPublisher()).take(Duration.ofMillis(500))) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("resumableRSocketStates") + void resumableRSocketsNotDisposedOnMissingKeepAlives( + Supplier testDataSupplier) { + ResumableRSocketState rSocketState = testDataSupplier.get(); + RSocket rSocket = rSocketState.rSocket(); + List errors = rSocketState.errors().errors(); + TestDuplexConnection connection = rSocketState.connection(); + + Mono.delay(Duration.ofMillis(500)).block(); + + Assertions.assertThat(rSocket.isDisposed()).isFalse(); + Assertions.assertThat(errors).hasSize(0); + Assertions.assertThat(connection.isDisposed()).isTrue(); + } + + private boolean keepAliveFrame(ByteBuf frame) { + return FrameHeaderFlyweight.frameType(frame) == FrameType.KEEPALIVE; + } + private boolean keepAliveFrameWithRespondFlag(ByteBuf frame) { - return FrameHeaderFlyweight.frameType(frame) == FrameType.KEEPALIVE - && KeepAliveFrameFlyweight.respondFlag(frame); + return keepAliveFrame(frame) && KeepAliveFrameFlyweight.respondFlag(frame); } private boolean keepAliveFrameWithoutRespondFlag(ByteBuf frame) { - return FrameHeaderFlyweight.frameType(frame) == FrameType.KEEPALIVE - && !KeepAliveFrameFlyweight.respondFlag(frame); + return keepAliveFrame(frame) && !KeepAliveFrameFlyweight.respondFlag(frame); + } + + static class RSocketState { + private final RSocket rSocket; + private final Errors errors; + private final TestDuplexConnection connection; + + public RSocketState(RSocket rSocket, Errors errors, TestDuplexConnection connection) { + this.rSocket = rSocket; + this.errors = errors; + this.connection = connection; + } + + public TestDuplexConnection connection() { + return connection; + } + + public RSocket rSocket() { + return rSocket; + } + + public Errors errors() { + return errors; + } } - static class TestData { + static class ResumableRSocketState { private final RSocket rSocket; private final Errors errors; private final TestDuplexConnection connection; + private final ResumableDuplexConnection resumableDuplexConnection; - public TestData(RSocket rSocket, Errors errors, TestDuplexConnection connection) { + public ResumableRSocketState( + RSocket rSocket, + Errors errors, + TestDuplexConnection connection, + ResumableDuplexConnection resumableDuplexConnection) { this.rSocket = rSocket; this.errors = errors; this.connection = connection; + this.resumableDuplexConnection = resumableDuplexConnection; } public TestDuplexConnection connection() { return connection; } + public ResumableDuplexConnection resumableDuplexConnection() { + return resumableDuplexConnection; + } + public RSocket rSocket() { return rSocket; } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java index a697524b3..cf21e64bd 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java @@ -2,6 +2,7 @@ import io.netty.buffer.ByteBuf; import io.netty.channel.Channel; +import io.netty.channel.ChannelPromise; import io.netty.channel.EventLoop; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; @@ -84,22 +85,26 @@ class SendPublisher extends Flux { fuse = queue instanceof Fuseable.QueueSubscription; } - @SuppressWarnings("unchecked") - private void writeCleanup(V poll) { - if (requested != Long.MAX_VALUE) { - requested--; - } - requestedUpstream--; - pending--; + private ChannelPromise writeCleanupPromise(V poll) { + return channel + .newPromise() + .addListener( + future -> { + if (requested != Long.MAX_VALUE) { + requested--; + } + requestedUpstream--; + pending--; - InnerSubscriber is = (InnerSubscriber) INNER_SUBSCRIBER.get(SendPublisher.this); - if (is != null) { - is.tryRequestMoreUpstream(); - tryComplete(is); - } - if (poll.refCnt() > 0) { - ReferenceCountUtil.safeRelease(poll); - } + InnerSubscriber is = (InnerSubscriber) INNER_SUBSCRIBER.get(SendPublisher.this); + if (is != null) { + is.tryRequestMoreUpstream(); + tryComplete(is); + } + if (poll.refCnt() > 0) { + ReferenceCountUtil.safeRelease(poll); + } + }); } private void tryComplete(InnerSubscriber is) { @@ -230,29 +235,11 @@ private void drain() { int readableBytes = sizeOf.size(poll); pending++; if (channel.isWritable() && readableBytes <= channel.bytesBeforeUnwritable()) { - channel - .write(poll) - .addListener( - future -> { - if (future.cause() != null) { - onError(future.cause()); - } else { - writeCleanup(poll); - } - }); + channel.write(poll, writeCleanupPromise(poll)); scheduleFlush = true; } else { scheduleFlush = false; - channel - .writeAndFlush(poll) - .addListener( - future -> { - if (future.cause() != null) { - onError(future.cause()); - } else { - writeCleanup(poll); - } - }); + channel.writeAndFlush(poll, writeCleanupPromise(poll)); } tryRequestMoreUpstream(); From 0732c6a2938646cd77cff9f79ffcb43d8594d602 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20Zaj=C4=85czkowski?= Date: Wed, 8 May 2019 20:07:57 +0200 Subject: [PATCH 038/181] [#588] Add OpenJDK 11 and 12 build on Travis (#627) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #588. Signed-off-by: Marcin Zajączkowski --- .travis.yml | 11 +++++++---- artifactory.gradle | 3 +++ bintray.gradle | 3 +++ 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8e0789f83..2a4ab71da 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,9 +16,13 @@ --- language: java -jdk: -- oraclejdk8 -# - oraclejdk9 +matrix: + include: + - jdk: oraclejdk8 + - jdk: openjdk11 + env: SKIP_RELEASE=true + - jdk: openjdk12 + env: SKIP_RELEASE=true env: global: @@ -37,4 +41,3 @@ cache: directories: - $HOME/.gradle/caches/ - $HOME/.gradle/wrapper/ - diff --git a/artifactory.gradle b/artifactory.gradle index 6e622a610..5626fc61e 100644 --- a/artifactory.gradle +++ b/artifactory.gradle @@ -35,6 +35,9 @@ if (project.hasProperty('bintrayUser') && project.hasProperty('bintrayKey')) { } } } + tasks.named("artifactoryPublish").configure { + onlyIf { System.getenv('SKIP_RELEASE') != "true" } + } } } } diff --git a/bintray.gradle b/bintray.gradle index 6fe0db84b..5015f94e4 100644 --- a/bintray.gradle +++ b/bintray.gradle @@ -55,6 +55,9 @@ if (project.hasProperty('bintrayUser') && project.hasProperty('bintrayKey') && } } } + tasks.named("bintrayUpload").configure { + onlyIf { System.getenv('SKIP_RELEASE') != "true" } + } } } } From 350da17ddeee6eb451a4950cb5e89d75402cf801 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Thu, 9 May 2019 01:25:20 -0700 Subject: [PATCH 039/181] Update README.md updating readme to 0.12 and new PayloadDecoder.* schema --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 8a9f85e11..5eacd94f0 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ Example: ```groovy dependencies { - implementation 'io.rsocket:rsocket-core:0.11.14' - implementation 'io.rsocket:rsocket-transport-netty:0.11.14' -// implementation 'io.rsocket:rsocket-core:0.11.15.BUILD-SNAPSHOT' -// implementation 'io.rsocket:rsocket-transport-netty:0.11.15.BUILD-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:0.12.2-RC2' + implementation 'io.rsocket:rsocket-transport-netty:0.12.2-RC2' +// implementation 'io.rsocket:rsocket-core:0.12.2-RC3-SNAPSHOT' +// implementation 'io.rsocket:rsocket-transport-netty:0.12.2-RC3-SNAPSHOT' } ``` @@ -91,7 +91,7 @@ or you will get a memory leak. Used correctly this will reduce latency and incre ```java RSocketFactory.receive() // Enable Zero Copy - .payloadDecoder(Frame::retain) + .frameDecoder(PayloadDecoder.ZERO_COPY) .acceptor(new PingHandler()) .transport(TcpServerTransport.create(7878)) .start() @@ -105,7 +105,7 @@ RSocketFactory.receive() Mono client = RSocketFactory.connect() // Enable Zero Copy - .payloadDecoder(Frame::retain) + .frameDecoder(PayloadDecoder.ZERO_COPY) .transport(TcpClientTransport.create(7878)) .start(); ``` From aa37d79dae41295120fc3c3fef0d0d140fd327a2 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Mon, 13 May 2019 20:58:45 +0300 Subject: [PATCH 040/181] actually stop keep-alive on closed connection Signed-off-by: Maksym Ostroverkhov --- .../src/main/java/io/rsocket/keepalive/KeepAliveHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java index b8ba8d5d0..2535c342b 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveHandler.java @@ -25,7 +25,7 @@ public KeepAliveFramesAcceptor start( KeepAliveSupport keepAliveSupport, Consumer onSendKeepAliveFrame, Consumer onTimeout) { - duplexConnection.onClose().subscribe(v -> keepAliveSupport.stop()); + duplexConnection.onClose().doFinally(s -> keepAliveSupport.stop()).subscribe(); return keepAliveSupport .onSendKeepAliveFrame(onSendKeepAliveFrame) .onTimeout(onTimeout) From 62d0ad73c005701994aa65e6bbf95298e5d2e4dc Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Tue, 14 May 2019 20:48:23 +0300 Subject: [PATCH 041/181] try fix travis build for java11/12 Signed-off-by: Maksym Ostroverkhov --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index bf9901506..b2b3f5378 100644 --- a/build.gradle +++ b/build.gradle @@ -116,6 +116,10 @@ subprojects { systemProperty "io.netty.leakDetection.level", "ADVANCED" } + + tasks.named("javadoc").configure { + onlyIf { System.getenv('SKIP_RELEASE') != "true" } + } } plugins.withType(JavaLibraryPlugin) { From d47629147dd1a4d41c7c8d5af3d80838e01d3ba5 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 16 May 2019 08:19:33 +0300 Subject: [PATCH 042/181] Example of resumable file transfer (#631) * resumable file transfer example Signed-off-by: Maksym Ostroverkhov * formatter Signed-off-by: Maksym Ostroverkhov * remove code Signed-off-by: Maksym Ostroverkhov --- .../java/io/rsocket/resume/ClientResume.java | 2 +- .../examples/transport/tcp/resume/Files.java | 134 ++++++++++++++++++ .../tcp/resume/ResumeFileTransfer.java | 120 ++++++++++++++++ .../examples/transport/tcp/resume/readme.md | 29 ++++ rsocket-examples/src/main/resources/lorem.txt | 32 +++++ 5 files changed, 316 insertions(+), 1 deletion(-) create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/Files.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/readme.md create mode 100644 rsocket-examples/src/main/resources/lorem.txt diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java index a8f9eba05..415a77f92 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientResume.java @@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf; import java.time.Duration; -class ClientResume { +public class ClientResume { private final Duration sessionDuration; private final ByteBuf resumeToken; 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/tcp/resume/Files.java new file mode 100644 index 000000000..e6867f8b5 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/Files.java @@ -0,0 +1,134 @@ +package io.rsocket.examples.transport.tcp.resume; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.rsocket.Payload; +import java.io.*; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.publisher.Flux; +import reactor.core.publisher.SynchronousSink; + +class Files { + + public static Flux fileSource(String fileName, int chunkSizeBytes) { + return Flux.generate( + () -> new FileState(fileName, chunkSizeBytes), FileState::consumeNext, FileState::dispose); + } + + public static Subscriber fileSink(String fileName, int windowSize) { + return new Subscriber() { + Subscription s; + int requests = windowSize; + OutputStream outputStream; + int receivedBytes; + int receivedCount; + + @Override + public void onSubscribe(Subscription s) { + this.s = s; + this.s.request(requests); + } + + @Override + public void onNext(Payload payload) { + ByteBuf data = payload.data(); + receivedBytes += data.readableBytes(); + receivedCount += 1; + System.out.println( + "Received file chunk: " + receivedCount + ". Total size: " + receivedBytes); + if (outputStream == null) { + outputStream = open(fileName); + } + write(outputStream, data); + payload.release(); + + requests--; + if (requests == windowSize / 2) { + requests += windowSize; + s.request(windowSize); + } + } + + private void write(OutputStream outputStream, ByteBuf byteBuf) { + try { + byteBuf.readBytes(outputStream, byteBuf.readableBytes()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Override + public void onError(Throwable t) { + close(outputStream); + } + + @Override + public void onComplete() { + close(outputStream); + } + + private OutputStream open(String filename) { + try { + /*do not buffer for demo purposes*/ + return new FileOutputStream(filename); + } catch (FileNotFoundException e) { + throw new RuntimeException(e); + } + } + + private void close(OutputStream stream) { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + } + } + } + }; + } + + private static class FileState { + private final String fileName; + private final int chunkSizeBytes; + private BufferedInputStream inputStream; + private byte[] chunkBytes; + + public FileState(String fileName, int chunkSizeBytes) { + this.fileName = fileName; + this.chunkSizeBytes = chunkSizeBytes; + } + + public 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]; + } + try { + int consumedBytes = inputStream.read(chunkBytes); + if (consumedBytes == -1) { + sink.complete(); + } else { + sink.next(Unpooled.copiedBuffer(chunkBytes, 0, consumedBytes)); + } + } catch (IOException e) { + sink.error(e); + } + return this; + } + + public void dispose() { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + } + } + } + } +} 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 new file mode 100644 index 000000000..df8e801a6 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/ResumeFileTransfer.java @@ -0,0 +1,120 @@ +package io.rsocket.examples.transport.tcp.resume; + +import io.rsocket.AbstractRSocket; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.RSocketFactory; +import io.rsocket.resume.ClientResume; +import io.rsocket.resume.PeriodicResumeStrategy; +import io.rsocket.resume.ResumeStrategy; +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 org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class ResumeFileTransfer { + + public static void main(String[] args) { + RequestCodec requestCodec = new RequestCodec(); + + CloseableChannel server = + RSocketFactory.receive() + .resume() + .resumeSessionDuration(Duration.ofMinutes(5)) + .acceptor((setup, rSocket) -> Mono.just(new FileServer(requestCodec))) + .transport(TcpServerTransport.create("localhost", 8000)) + .start() + .block(); + + RSocket client = + RSocketFactory.connect() + .resume() + .resumeStrategy( + () -> new VerboseResumeStrategy(new PeriodicResumeStrategy(Duration.ofSeconds(1)))) + .resumeSessionDuration(Duration.ofMinutes(5)) + .transport(TcpClientTransport.create("localhost", 8001)) + .start() + .block(); + + client + .requestStream(requestCodec.encode(new Request(16, "lorem.txt"))) + .doFinally(s -> server.dispose()) + .subscribe(Files.fileSink("rsocket-examples/out/lorem_output.txt", 256)); + + server.onClose().block(); + } + + private static class FileServer extends AbstractRSocket { + private final RequestCodec requestCodec; + + public FileServer(RequestCodec requestCodec) { + this.requestCodec = requestCodec; + } + + @Override + public Flux requestStream(Payload payload) { + Request request = requestCodec.decode(payload); + payload.release(); + String fileName = request.getFileName(); + int chunkSize = request.getChunkSize(); + + Flux ticks = Flux.interval(Duration.ofMillis(500)).onBackpressureDrop(); + + return Files.fileSource(fileName, chunkSize) + .map(DefaultPayload::create) + .zipWith(ticks, (p, tick) -> p); + } + } + + private static class VerboseResumeStrategy implements ResumeStrategy { + private final ResumeStrategy resumeStrategy; + + public VerboseResumeStrategy(ResumeStrategy resumeStrategy) { + this.resumeStrategy = resumeStrategy; + } + + @Override + public Publisher apply(ClientResume clientResume, Throwable throwable) { + return Flux.from(resumeStrategy.apply(clientResume, throwable)) + .doOnNext(v -> System.out.println("Disconnected. Trying to resume connection...")); + } + } + + 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; + } + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/readme.md b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/readme.md new file mode 100644 index 000000000..55e761fe8 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/readme.md @@ -0,0 +1,29 @@ +1. Start socat. It is used for emulation of transport disconnects + +`socat -d TCP-LISTEN:8001,fork,reuseaddr TCP:localhost:8000` + +2. start `ResumeFileTransfer.main` + +3. terminate/start socat periodically for session resumption + +`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 connection... +Disconnected. Trying to resume connection... +Disconnected. Trying to resume connection... +Disconnected. Trying to resume connection... +Disconnected. Trying to resume connection... +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/out/lorem_output.txt` in chunks of 16 bytes every 500 millis diff --git a/rsocket-examples/src/main/resources/lorem.txt b/rsocket-examples/src/main/resources/lorem.txt new file mode 100644 index 000000000..e035ea86d --- /dev/null +++ b/rsocket-examples/src/main/resources/lorem.txt @@ -0,0 +1,32 @@ +Alteration literature to or an sympathize mr imprudence. Of is ferrars subject as enjoyed or tedious cottage. +Procuring as in resembled by in agreeable. Next long no gave mr eyes. Admiration advantages no he celebrated so pianoforte unreserved. +Not its herself forming charmed amiable. Him why feebly expect future now. + +Situation admitting promotion at or to perceived be. Mr acuteness we as estimable enjoyment up. +An held late as felt know. Learn do allow solid to grave. Middleton suspicion age her attention. +Chiefly several bed its wishing. Is so moments on chamber pressed to. Doubtful yet way properly answered humanity its desirous. + Minuter believe service arrived civilly add all. Acuteness allowance an at eagerness favourite in extensive exquisite ye. + + Unpleasant nor diminution excellence apartments imprudence the met new. Draw part them he an to he roof only. + Music leave say doors him. Tore bred form if sigh case as do. Staying he no looking if do opinion. + Sentiments way understood end partiality and his. + + Ladyship it daughter securing procured or am moreover mr. Put sir she exercise vicinity cheerful wondered. + Continual say suspicion provision you neglected sir curiosity unwilling. Simplicity end themselves increasing led day sympathize yet. + General windows effects not are drawing man garrets. Common indeed garden you his ladies out yet. Preference imprudence contrasted to remarkably in on. + Taken now you him trees tears any. Her object giving end sister except oppose. + + No comfort do written conduct at prevent manners on. Celebrated contrasted discretion him sympathize her collecting occasional. + Do answered bachelor occasion in of offended no concerns. Supply worthy warmth branch of no ye. Voice tried known to as my to. + Though wished merits or be. Alone visit use these smart rooms ham. No waiting in on enjoyed placing it inquiry. + + So insisted received is occasion advanced honoured. Among ready to which up. Attacks smiling and may out assured moments man nothing outward. + Thrown any behind afford either the set depend one temper. Instrument melancholy in acceptance collecting frequently be if. + Zealously now pronounce existence add you instantly say offending. Merry their far had widen was. Concerns no in expenses raillery formerly. + + As am hastily invited settled at limited civilly fortune me. Really spring in extent an by. Judge but built gay party world. + Of so am he remember although required. Bachelor unpacked be advanced at. Confined in declared marianne is vicinity. + + In alteration insipidity impression by travelling reasonable up motionless. Of regard warmth by unable sudden garden ladies. + No kept hung am size spot no. Likewise led and dissuade rejoiced welcomed husbands boy. Do listening on he suspected resembled. + Water would still if to. Position boy required law moderate was may. \ No newline at end of file From 6b706f920743be39cb07d99ae39e05feabd27003 Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Mon, 20 May 2019 21:44:08 -0700 Subject: [PATCH 043/181] Update Californium-SR8 and remove redundant SendPublisher (#632) * Update Californium-SR8 and remove redundant SendPublisher * Update boringssl netty bridge * Remove unnecessary retain * Reformat with google style and removed unused imports Signed-off-by: Stephane Maldini --- build.gradle | 6 +- .../transport/netty/SendPublisher.java | 299 ------------------ .../transport/netty/TcpDuplexConnection.java | 26 +- .../netty/WebsocketDuplexConnection.java | 37 +-- 4 files changed, 17 insertions(+), 351 deletions(-) delete mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java diff --git a/build.gradle b/build.gradle index b2b3f5378..7a6cfcebb 100644 --- a/build.gradle +++ b/build.gradle @@ -29,11 +29,11 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = 'Californium-SR5' + ext['reactor-bom.version'] = 'Californium-SR8' ext['logback.version'] = '1.2.3' ext['findbugs.version'] = '3.0.2' - ext['netty.version'] = '4.1.31.Final' - ext['netty-boringssl.version'] = '2.0.18.Final' + ext['netty.version'] = '4.1.36.Final' + ext['netty-boringssl.version'] = '2.0.25.Final' ext['hdrhistogram.version'] = '2.1.10' ext['mockito.version'] = '2.25.1' ext['slf4j.version'] = '1.7.25' diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java deleted file mode 100644 index cf21e64bd..000000000 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/SendPublisher.java +++ /dev/null @@ -1,299 +0,0 @@ -package io.rsocket.transport.netty; - -import io.netty.buffer.ByteBuf; -import io.netty.channel.Channel; -import io.netty.channel.ChannelPromise; -import io.netty.channel.EventLoop; -import io.netty.util.ReferenceCountUtil; -import io.netty.util.ReferenceCounted; -import java.nio.channels.ClosedChannelException; -import java.util.Queue; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -import java.util.function.Function; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.CoreSubscriber; -import reactor.core.Fuseable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Operators; -import reactor.netty.FutureMono; -import reactor.util.concurrent.Queues; - -class SendPublisher extends Flux { - - private static final AtomicIntegerFieldUpdater WIP = - AtomicIntegerFieldUpdater.newUpdater(SendPublisher.class, "wip"); - - private static final int MAX_SIZE = Queues.SMALL_BUFFER_SIZE; - private static final int REFILL_SIZE = MAX_SIZE / 2; - private static final AtomicReferenceFieldUpdater INNER_SUBSCRIBER = - AtomicReferenceFieldUpdater.newUpdater(SendPublisher.class, Object.class, "innerSubscriber"); - private static final AtomicIntegerFieldUpdater TERMINATED = - AtomicIntegerFieldUpdater.newUpdater(SendPublisher.class, "terminated"); - private final Publisher source; - private final Channel channel; - private final EventLoop eventLoop; - - private final Queue queue; - private final AtomicBoolean completed = new AtomicBoolean(); - private final Function transformer; - private final SizeOf sizeOf; - - @SuppressWarnings("unused") - private volatile int terminated; - - private int pending; - - @SuppressWarnings("unused") - private volatile int wip; - - @SuppressWarnings("unused") - private volatile Object innerSubscriber; - - private long requested; - - private long requestedUpstream = MAX_SIZE; - - private boolean fuse; - - @SuppressWarnings("unchecked") - SendPublisher( - Publisher source, - Channel channel, - Function transformer, - SizeOf sizeOf) { - this(Queues.small().get(), source, channel, transformer, sizeOf); - } - - @SuppressWarnings("unchecked") - SendPublisher( - Queue queue, - Publisher source, - Channel channel, - Function transformer, - SizeOf sizeOf) { - this.source = source; - this.channel = channel; - this.queue = queue; - this.eventLoop = channel.eventLoop(); - this.transformer = transformer; - this.sizeOf = sizeOf; - - fuse = queue instanceof Fuseable.QueueSubscription; - } - - private ChannelPromise writeCleanupPromise(V poll) { - return channel - .newPromise() - .addListener( - future -> { - if (requested != Long.MAX_VALUE) { - requested--; - } - requestedUpstream--; - pending--; - - InnerSubscriber is = (InnerSubscriber) INNER_SUBSCRIBER.get(SendPublisher.this); - if (is != null) { - is.tryRequestMoreUpstream(); - tryComplete(is); - } - if (poll.refCnt() > 0) { - ReferenceCountUtil.safeRelease(poll); - } - }); - } - - private void tryComplete(InnerSubscriber is) { - if (pending == 0 - && completed.get() - && queue.isEmpty() - && terminated == 0 - && !is.pendingFlush.get()) { - TERMINATED.set(SendPublisher.this, 1); - is.destination.onComplete(); - } - } - - @Override - public void subscribe(CoreSubscriber destination) { - InnerSubscriber innerSubscriber = new InnerSubscriber(destination); - if (!INNER_SUBSCRIBER.compareAndSet(this, null, innerSubscriber)) { - Operators.error( - destination, new IllegalStateException("SendPublisher only allows one subscription")); - } else { - InnerSubscription innerSubscription = new InnerSubscription(innerSubscriber); - destination.onSubscribe(innerSubscription); - source.subscribe(innerSubscriber); - } - } - - @FunctionalInterface - interface SizeOf { - int size(V v); - } - - private class InnerSubscriber implements Subscriber { - final CoreSubscriber destination; - volatile Subscription s; - private AtomicBoolean pendingFlush = new AtomicBoolean(); - - private InnerSubscriber(CoreSubscriber destination) { - this.destination = destination; - FutureMono.from(channel.closeFuture()) - .doFinally(s -> onError(new ClosedChannelException())) - .subscribe(); - } - - @Override - public void onSubscribe(Subscription s) { - this.s = s; - s.request(MAX_SIZE); - tryDrain(); - } - - @Override - public void onNext(ByteBuf t) { - if (terminated == 0) { - if (!fuse && !queue.offer(t)) { - throw new IllegalStateException("missing back pressure"); - } - tryDrain(); - } - } - - @Override - public void onError(Throwable t) { - if (TERMINATED.compareAndSet(SendPublisher.this, 0, 1)) { - try { - s.cancel(); - destination.onError(t); - } finally { - ByteBuf byteBuf = queue.poll(); - while (byteBuf != null) { - ReferenceCountUtil.safeRelease(byteBuf); - byteBuf = queue.poll(); - } - } - } - } - - @Override - public void onComplete() { - if (completed.compareAndSet(false, true)) { - tryDrain(); - } - } - - private void tryRequestMoreUpstream() { - if (requestedUpstream <= REFILL_SIZE && s != null) { - long u = MAX_SIZE - requestedUpstream; - requestedUpstream = Operators.addCap(requestedUpstream, u); - s.request(u); - } - } - - private void flush() { - try { - channel.flush(); - pendingFlush.set(false); - tryComplete(this); - } catch (Throwable t) { - onError(t); - } - } - - private void tryDrain() { - if (terminated == 0 && WIP.getAndIncrement(SendPublisher.this) == 0) { - try { - if (eventLoop.inEventLoop()) { - drain(); - } else { - eventLoop.execute(this::drain); - } - } catch (Throwable t) { - onError(t); - } - } - } - - private void drain() { - try { - boolean scheduleFlush; - int missed = 1; - for (; ; ) { - scheduleFlush = false; - - long r = Math.min(requested, requestedUpstream); - while (r-- > 0) { - ByteBuf ByteBuf = queue.poll(); - if (ByteBuf != null && terminated == 0) { - V poll = transformer.apply(ByteBuf); - int readableBytes = sizeOf.size(poll); - pending++; - if (channel.isWritable() && readableBytes <= channel.bytesBeforeUnwritable()) { - channel.write(poll, writeCleanupPromise(poll)); - scheduleFlush = true; - } else { - scheduleFlush = false; - channel.writeAndFlush(poll, writeCleanupPromise(poll)); - } - - tryRequestMoreUpstream(); - } else { - break; - } - } - - if (scheduleFlush) { - pendingFlush.set(true); - eventLoop.execute(this::flush); - } - - if (terminated == 1) { - break; - } - - missed = WIP.addAndGet(SendPublisher.this, -missed); - if (missed == 0) { - break; - } - } - } catch (Throwable t) { - onError(t); - } - } - } - - private class InnerSubscription implements Subscription { - private final InnerSubscriber innerSubscriber; - - private InnerSubscription(InnerSubscriber innerSubscriber) { - this.innerSubscriber = innerSubscriber; - } - - @Override - public void request(long n) { - if (eventLoop.inEventLoop()) { - requested = Operators.addCap(n, requested); - innerSubscriber.tryDrain(); - } else { - eventLoop.execute(() -> request(n)); - } - } - - @Override - public void cancel() { - TERMINATED.set(SendPublisher.this, 1); - while (!queue.isEmpty()) { - ByteBuf poll = queue.poll(); - if (poll != null) { - ReferenceCountUtil.safeRelease(poll); - } - } - } - } -} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java index ffa5503c8..c9c29f0a9 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java @@ -23,7 +23,6 @@ import io.rsocket.internal.BaseDuplexConnection; import java.util.Objects; import org.reactivestreams.Publisher; -import reactor.core.Fuseable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty.Connection; @@ -77,30 +76,15 @@ public Flux receive() { @Override public Mono send(Publisher frames) { - return Flux.from(frames) - .transform( - frameFlux -> { - if (frameFlux instanceof Fuseable.QueueSubscription) { - Fuseable.QueueSubscription queueSubscription = - (Fuseable.QueueSubscription) frameFlux; - queueSubscription.requestFusion(Fuseable.ASYNC); - return new SendPublisher<>( - queueSubscription, - frameFlux, - connection.channel(), - this::encode, - ByteBuf::readableBytes); - } else { - return new SendPublisher<>( - frameFlux, connection.channel(), this::encode, ByteBuf::readableBytes); - } - }) - .then(); + if (frames instanceof Mono) { + return connection.outbound().sendObject(((Mono) frames).map(this::encode)).then(); + } + return connection.outbound().send(Flux.from(frames).map(this::encode)).then(); } private ByteBuf encode(ByteBuf frame) { if (encodeLength) { - return FrameLengthFlyweight.encode(allocator, frame.readableBytes(), frame).retain(); + return FrameLengthFlyweight.encode(allocator, frame.readableBytes(), frame); } else { return frame; } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java index f83725e47..ead297928 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java @@ -21,11 +21,9 @@ import io.rsocket.internal.BaseDuplexConnection; import java.util.Objects; import org.reactivestreams.Publisher; -import reactor.core.Fuseable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.netty.Connection; -import reactor.util.concurrent.Queues; /** * An implementation of {@link DuplexConnection} that connects via a Websocket. @@ -69,32 +67,15 @@ public Flux receive() { @Override public Mono send(Publisher frames) { - return Flux.from(frames) - .transform( - frameFlux -> { - if (frameFlux instanceof Fuseable.QueueSubscription) { - Fuseable.QueueSubscription queueSubscription = - (Fuseable.QueueSubscription) frameFlux; - queueSubscription.requestFusion(Fuseable.ASYNC); - return new SendPublisher<>( - queueSubscription, - frameFlux, - connection.channel(), - this::toBinaryWebSocketFrame, - binaryWebSocketFrame -> binaryWebSocketFrame.content().readableBytes()); - } else { - return new SendPublisher<>( - Queues.small().get(), - frameFlux, - connection.channel(), - this::toBinaryWebSocketFrame, - binaryWebSocketFrame -> binaryWebSocketFrame.content().readableBytes()); - } - }) + if (frames instanceof Mono) { + return connection + .outbound() + .sendObject(((Mono) frames).map(BinaryWebSocketFrame::new)) + .then(); + } + return connection + .outbound() + .sendObject(Flux.from(frames).map(BinaryWebSocketFrame::new)) .then(); } - - private BinaryWebSocketFrame toBinaryWebSocketFrame(ByteBuf frame) { - return new BinaryWebSocketFrame(frame.retain()); - } } From 71915d4f0c16f1107b8b09f9555eff4bfe07e499 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Tue, 21 May 2019 08:23:16 +0300 Subject: [PATCH 044/181] Fix bug when process OOMs due to retained LimitableRequestPublishers in RSocketServer (#638) * remove stream subscription on cancel as hookFinally() callback is executed only on BaseSubscriber.cancel() Signed-off-by: Maksym Ostroverkhov * workaround reactor-netty bug when connections are not closed on DisposableChannel close Signed-off-by: Maksym Ostroverkhov * responder stream/channel: calculate initialRequestN before applying handler Signed-off-by: Maksym Ostroverkhov * disable LimitableRequestPublisher transport request coordination Signed-off-by: Maksym Ostroverkhov * update version Signed-off-by: Maksym Ostroverkhov * disable rsocket-transport request coordination tests Signed-off-by: Maksym Ostroverkhov * add LimitableRequestPublisher.wrap(Flux) remove rsocket-transport request coordination tests Signed-off-by: Maksym Ostroverkhov * use correct RSocket version Signed-off-by: Maksym Ostroverkhov * Revert "workaround reactor-netty bug when connections are not closed on DisposableChannel close" This reverts commit 67bea790 Signed-off-by: Maksym Ostroverkhov --- .../main/java/io/rsocket/RSocketClient.java | 13 +-- .../main/java/io/rsocket/RSocketServer.java | 28 +++--- .../internal/LimitableRequestPublisher.java | 4 + .../java/io/rsocket/RSocketClientTest.java | 29 ------ .../java/io/rsocket/RSocketServerTest.java | 88 ------------------- 5 files changed, 17 insertions(+), 145 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java index b4c9dbb20..539e104b4 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketClient.java @@ -81,15 +81,8 @@ class RSocketClient implements RSocket { this.sendProcessor = new UnboundedProcessor<>(); connection.onClose().doFinally(signalType -> terminate()).subscribe(null, errorConsumer); - - sendProcessor - .doOnRequest( - r -> { - for (LimitableRequestPublisher lrp : senders.values()) { - lrp.increaseInternalLimit(r); - } - }) - .transform(connection::send) + connection + .send(sendProcessor) .doFinally(this::handleSendProcessorCancel) .subscribe(null, this::handleSendProcessorError); @@ -329,7 +322,7 @@ public void accept(long n) { .transform( f -> { LimitableRequestPublisher wrapped = - LimitableRequestPublisher.wrap(f, sendProcessor.available()); + LimitableRequestPublisher.wrap(f); // Need to set this to one for first the frame wrapped.request(1); senders.put(streamId, wrapped); diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java index 451ed7b42..6ce5ef88d 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketServer.java @@ -97,14 +97,8 @@ class RSocketServer implements ResponderRSocket { // connections this.sendProcessor = new UnboundedProcessor<>(); - sendProcessor - .doOnRequest( - r -> { - for (LimitableRequestPublisher lrp : sendingLimitableSubscriptions.values()) { - lrp.increaseInternalLimit(r); - } - }) - .transform(connection::send) + connection + .send(sendProcessor) .doFinally(this::handleSendProcessorCancel) .subscribe(null, this::handleSendProcessorError); @@ -322,16 +316,14 @@ private void handleFrame(ByteBuf frame) { handleRequestN(streamId, frame); break; case REQUEST_STREAM: - handleStream( - streamId, - requestStream(payloadDecoder.apply(frame)), - RequestStreamFrameFlyweight.initialRequestN(frame)); + int streamInitialRequestN = RequestStreamFrameFlyweight.initialRequestN(frame); + Payload streamPayload = payloadDecoder.apply(frame); + handleStream(streamId, requestStream(streamPayload), streamInitialRequestN); break; case REQUEST_CHANNEL: - handleChannel( - streamId, - payloadDecoder.apply(frame), - RequestChannelFrameFlyweight.initialRequestN(frame)); + int channelInitialRequestN = RequestChannelFrameFlyweight.initialRequestN(frame); + Payload channelPayload = payloadDecoder.apply(frame); + handleChannel(streamId, channelPayload, channelInitialRequestN); break; case METADATA_PUSH: metadataPush(payloadDecoder.apply(frame)); @@ -459,7 +451,7 @@ private void handleStream(int streamId, Flux response, int initialReque .transform( frameFlux -> { LimitableRequestPublisher payloads = - LimitableRequestPublisher.wrap(frameFlux, sendProcessor.available()); + LimitableRequestPublisher.wrap(frameFlux); sendingLimitableSubscriptions.put(streamId, payloads); payloads.request( initialRequestN >= Integer.MAX_VALUE ? Long.MAX_VALUE : initialRequestN); @@ -535,7 +527,7 @@ private void handleCancelFrame(int streamId) { Subscription subscription = sendingSubscriptions.remove(streamId); if (subscription == null) { - subscription = sendingLimitableSubscriptions.get(streamId); + subscription = sendingLimitableSubscriptions.remove(streamId); } if (subscription != null) { diff --git a/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java b/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java index 2eafd3d61..8adb7542a 100755 --- a/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java @@ -56,6 +56,10 @@ public static LimitableRequestPublisher wrap(Publisher source, long pr return new LimitableRequestPublisher<>(source, prefetch); } + public static LimitableRequestPublisher wrap(Publisher source) { + return wrap(source, Long.MAX_VALUE); + } + @Override public void subscribe(CoreSubscriber destination) { synchronized (this) { diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java index 2494cbbca..c60dba312 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java @@ -28,15 +28,12 @@ import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.*; -import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestSubscriber; import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; import java.time.Duration; import java.util.ArrayList; import java.util.List; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.stream.Collectors; import org.assertj.core.api.Assertions; import org.junit.Rule; @@ -214,32 +211,6 @@ public void testChannelRequestServerSideCancellation() { Assertions.assertThat(request.isDisposed()).isTrue(); } - @Test(timeout = 2_000) - @SuppressWarnings("unchecked") - public void - testClientSideRequestChannelShouldNotHangInfinitelySendingElementsAndShouldProduceDataValuingConnectionBackpressure() { - final Queue requests = new ConcurrentLinkedQueue<>(); - rule.connection.dispose(); - rule.connection = new TestDuplexConnection(); - rule.connection.setInitialSendRequestN(256); - rule.init(); - - rule.socket - .requestChannel( - Flux.generate(s -> s.next(EmptyPayload.INSTANCE)).doOnRequest(requests::add)) - .subscribe(); - - int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); - - assertThat("Unexpected error.", rule.errors, is(empty())); - - rule.connection.addToReceivedBuffer( - RequestNFrameFlyweight.encode(ByteBufAllocator.DEFAULT, streamId, 2)); - rule.connection.addToReceivedBuffer( - RequestNFrameFlyweight.encode(ByteBufAllocator.DEFAULT, streamId, Integer.MAX_VALUE)); - Assertions.assertThat(requests).containsOnly(1L, 2L, 253L); - } - public int sendRequestResponse(Publisher response) { Subscriber sub = TestSubscriber.create(); response.subscribe(sub); diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java index 9f2541975..32c0406b9 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java @@ -29,17 +29,12 @@ import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; import java.util.Collection; -import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; -import org.assertj.core.api.Assertions; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; -import org.mockito.Mockito; -import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class RSocketServerTest { @@ -111,89 +106,6 @@ public Mono requestResponse(Payload payload) { assertThat("Subscription not cancelled.", cancelled.get(), is(true)); } - @Test(timeout = 2_000) - @SuppressWarnings("unchecked") - public void - testServerSideRequestStreamShouldNotHangInfinitelySendingElementsAndShouldProduceDataValuingConnectionBackpressure() { - final int streamId = 5; - final Queue received = new ConcurrentLinkedQueue<>(); - final Queue requests = new ConcurrentLinkedQueue<>(); - - rule.setAcceptingSocket( - new AbstractRSocket() { - @Override - public Flux requestStream(Payload payload) { - return Flux.generate(s -> s.next(payload.retain())).doOnRequest(requests::add); - } - }, - 256); - - rule.sendRequest(streamId, FrameType.REQUEST_STREAM); - - assertThat("Unexpected error.", rule.errors, is(empty())); - - Subscriber next = rule.connection.getSendSubscribers().iterator().next(); - - Mockito.doAnswer( - invocation -> { - received.add(invocation.getArgument(0)); - - if (received.size() == 256) { - throw new RuntimeException(); - } - - return null; - }) - .when(next) - .onNext(Mockito.any()); - - rule.connection.addToReceivedBuffer( - RequestNFrameFlyweight.encode(ByteBufAllocator.DEFAULT, streamId, Integer.MAX_VALUE)); - Assertions.assertThat(requests).containsOnly(1L, 2L, 253L); - } - - @Test(timeout = 2_000) - @SuppressWarnings("unchecked") - public void - testServerSideRequestChannelShouldNotHangInfinitelySendingElementsAndShouldProduceDataValuingConnectionBackpressure() { - final int streamId = 5; - final Queue received = new ConcurrentLinkedQueue<>(); - final Queue requests = new ConcurrentLinkedQueue<>(); - - rule.setAcceptingSocket( - new AbstractRSocket() { - @Override - public Flux requestChannel(Publisher payload) { - return Flux.generate(s -> s.next(EmptyPayload.INSTANCE)) - .doOnRequest(requests::add); - } - }, - 256); - - rule.sendRequest(streamId, FrameType.REQUEST_CHANNEL); - - assertThat("Unexpected error.", rule.errors, is(empty())); - - Subscriber next = rule.connection.getSendSubscribers().iterator().next(); - - Mockito.doAnswer( - invocation -> { - received.add(invocation.getArgument(0)); - - if (received.size() == 256) { - throw new RuntimeException(); - } - - return null; - }) - .when(next) - .onNext(Mockito.any()); - - rule.connection.addToReceivedBuffer( - RequestNFrameFlyweight.encode(ByteBufAllocator.DEFAULT, streamId, Integer.MAX_VALUE)); - Assertions.assertThat(requests).containsOnly(1L, 2L, 253L); - } - public static class ServerSocketRule extends AbstractSocketRule { private RSocket acceptingSocket; From 9065434c54642829d09630cfb9fc863a587364f7 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 21 May 2019 01:24:32 -0400 Subject: [PATCH 045/181] Client-side acceptor access to ConnectionSetupPayload (#636) A client acceptor, especially if declared as a top-level class and a singleton can also benefit from access to the ConnectionSetupPayload. Signed-off-by: Rossen Stoyanchev --- .../main/java/io/rsocket/RSocketFactory.java | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 807e1e0a9..34c113481 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -42,6 +42,7 @@ import io.rsocket.util.EmptyPayload; import java.time.Duration; import java.util.Objects; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -94,6 +95,8 @@ public static class ClientRSocketFactory implements ClientTransportAcceptor { private Supplier> acceptor = () -> rSocket -> new AbstractRSocket() {}; + private BiFunction biAcceptor; + private Consumer errorConsumer = Throwable::printStackTrace; private int mtu = 0; private PluginRegistry plugins = new PluginRegistry(Plugins.defaultPlugins()); @@ -242,6 +245,12 @@ public ClientTransportAcceptor acceptor(Supplier> acc return StartClient::new; } + public ClientTransportAcceptor acceptor( + BiFunction biAcceptor) { + this.biAcceptor = biAcceptor; + return StartClient::new; + } + public ClientRSocketFactory fragment(int mtu) { this.mtu = mtu; return this; @@ -293,9 +302,27 @@ public Mono start() { keepAliveTimeout(), keepAliveHandler); + ByteBuf setupFrame = + SetupFrameFlyweight.encode( + allocator, + false, + keepAliveTickPeriod(), + keepAliveTimeout(), + resumeToken, + metadataMimeType, + dataMimeType, + setupPayload.sliceMetadata(), + setupPayload.sliceData()); + RSocket wrappedRSocketClient = plugins.applyClient(rSocketClient); - RSocket unwrappedServerSocket = acceptor.get().apply(wrappedRSocketClient); + RSocket unwrappedServerSocket; + if (biAcceptor != null) { + ConnectionSetupPayload setup = ConnectionSetupPayload.create(setupFrame); + unwrappedServerSocket = biAcceptor.apply(setup, wrappedRSocketClient); + } else { + unwrappedServerSocket = acceptor.get().apply(wrappedRSocketClient); + } RSocket wrappedRSocketServer = plugins.applyServer(unwrappedServerSocket); @@ -307,18 +334,6 @@ public Mono start() { payloadDecoder, errorConsumer); - ByteBuf setupFrame = - SetupFrameFlyweight.encode( - allocator, - false, - keepAliveTickPeriod(), - keepAliveTimeout(), - resumeToken, - metadataMimeType, - dataMimeType, - setupPayload.sliceMetadata(), - setupPayload.sliceData()); - return wrappedConnection.sendOne(setupFrame).thenReturn(wrappedRSocketClient); }); } From e6ed2513f8ddaa132700566f640da52efd77e4a2 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Wed, 22 May 2019 18:10:30 +0100 Subject: [PATCH 046/181] Fix logging formatting (#639) Signed-off-by: Yuri Schimke --- .../src/main/java/io/rsocket/resume/ClientRSocketSession.java | 2 +- .../src/main/java/io/rsocket/resume/ServerRSocketSession.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java index f798a1944..b347642e3 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -77,7 +77,7 @@ public ClientRSocketSession( errors .doOnNext( retryErr -> - logger.debug("Resumption reconnection error: {}", retryErr)) + logger.debug("Resumption reconnection error", retryErr)) .flatMap( retryErr -> Mono.from(reconnectOnError.apply(clientResume, retryErr)) diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java index ed91cc2ab..1a0605497 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -64,7 +64,7 @@ public ServerRSocketSession( .connectionErrors() .flatMap( err -> { - logger.debug("Starting session timeout due to error: {}", err); + logger.debug("Starting session timeout due to error", err); return newConnections .next() .doOnNext(c -> logger.debug("Connection after error: {}", c)) From 9a4668faa6a8c2ede7229ead76d1317fbc2d3eb0 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 23 May 2019 00:48:36 +0300 Subject: [PATCH 047/181] update version Signed-off-by: Maksym Ostroverkhov --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d5b97cda1..044cb3c77 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.12.2-RC3-SNAPSHOT +version=0.12.2-RC3 From 312f4ac5ada9a0d6bd00ea02c931a1eab348f127 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Wed, 22 May 2019 14:53:59 -0700 Subject: [PATCH 048/181] change version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 044cb3c77..5e14306b7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.12.2-RC3 +version=0.12.2-RC4-SNAPSHOT From a8f5a751a3541cebc7730759b7890888bc6c045b Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 30 May 2019 11:27:36 +0300 Subject: [PATCH 049/181] Rename internal components to match their purpose (#643) * Rename internal components to match their purpose Introduce addRequesterPlugin and addHandlerPlugin to RSocketFactory instead of addClientPlugin, addServerPlugin; derprecate latter Signed-off-by: Maksym Ostroverkhov --- .../main/java/io/rsocket/RSocketFactory.java | 74 ++++++++++++------- ...ocketClient.java => RSocketRequester.java} | 10 ++- ...ocketServer.java => RSocketResponder.java} | 8 +- .../io/rsocket/plugins/PluginRegistry.java | 40 ++++++++-- .../test/java/io/rsocket/KeepAliveTest.java | 16 ++-- ...a => RSocketRequesterTerminationTest.java} | 10 +-- ...entTest.java => RSocketRequesterTest.java} | 8 +- ...verTest.java => RSocketResponderTest.java} | 8 +- .../src/test/java/io/rsocket/RSocketTest.java | 8 +- .../java/io/rsocket/SetupRejectionTest.java | 8 +- 10 files changed, 119 insertions(+), 71 deletions(-) rename rsocket-core/src/main/java/io/rsocket/{RSocketClient.java => RSocketRequester.java} (99%) rename rsocket-core/src/main/java/io/rsocket/{RSocketServer.java => RSocketResponder.java} (98%) rename rsocket-core/src/test/java/io/rsocket/{RSocketClientTerminationTest.java => RSocketRequesterTerminationTest.java} (86%) rename rsocket-core/src/test/java/io/rsocket/{RSocketClientTest.java => RSocketRequesterTest.java} (98%) rename rsocket-core/src/test/java/io/rsocket/{RSocketServerTest.java => RSocketResponderTest.java} (97%) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 34c113481..1743bd6da 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -134,14 +134,25 @@ public ClientRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor inte plugins.addConnectionPlugin(interceptor); return this; } - + /** Deprecated. Use {@link #addRequesterPlugin(RSocketInterceptor)} instead */ + @Deprecated public ClientRSocketFactory addClientPlugin(RSocketInterceptor interceptor) { - plugins.addClientPlugin(interceptor); + return addRequesterPlugin(interceptor); + } + + public ClientRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor) { + plugins.addRequesterPlugin(interceptor); return this; } + /** Deprecated. Use {@link #addResponderPlugin(RSocketInterceptor)} instead */ + @Deprecated public ClientRSocketFactory addServerPlugin(RSocketInterceptor interceptor) { - plugins.addServerPlugin(interceptor); + return addResponderPlugin(interceptor); + } + + public ClientRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { + plugins.addResponderPlugin(interceptor); return this; } @@ -291,8 +302,8 @@ public Mono start() { ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(wrappedConnection, plugins); - RSocketClient rSocketClient = - new RSocketClient( + RSocketRequester rSocketRequester = + new RSocketRequester( allocator, multiplexer.asClientConnection(), payloadDecoder, @@ -314,27 +325,27 @@ public Mono start() { setupPayload.sliceMetadata(), setupPayload.sliceData()); - RSocket wrappedRSocketClient = plugins.applyClient(rSocketClient); + RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); - RSocket unwrappedServerSocket; + RSocket rSocketHandler; if (biAcceptor != null) { ConnectionSetupPayload setup = ConnectionSetupPayload.create(setupFrame); - unwrappedServerSocket = biAcceptor.apply(setup, wrappedRSocketClient); + rSocketHandler = biAcceptor.apply(setup, wrappedRSocketRequester); } else { - unwrappedServerSocket = acceptor.get().apply(wrappedRSocketClient); + rSocketHandler = acceptor.get().apply(wrappedRSocketRequester); } - RSocket wrappedRSocketServer = plugins.applyServer(unwrappedServerSocket); + RSocket wrappedRSocketHandler = plugins.applyResponder(rSocketHandler); - RSocketServer rSocketServer = - new RSocketServer( + RSocketResponder rSocketResponder = + new RSocketResponder( allocator, multiplexer.asServerConnection(), - wrappedRSocketServer, + wrappedRSocketHandler, payloadDecoder, errorConsumer); - return wrappedConnection.sendOne(setupFrame).thenReturn(wrappedRSocketClient); + return wrappedConnection.sendOne(setupFrame).thenReturn(wrappedRSocketRequester); }); } @@ -397,14 +408,25 @@ public ServerRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor inte plugins.addConnectionPlugin(interceptor); return this; } - + /** Deprecated. Use {@link #addRequesterPlugin(RSocketInterceptor)} instead */ + @Deprecated public ServerRSocketFactory addClientPlugin(RSocketInterceptor interceptor) { - plugins.addClientPlugin(interceptor); + return addRequesterPlugin(interceptor); + } + + public ServerRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor) { + plugins.addRequesterPlugin(interceptor); return this; } + /** Deprecated. Use {@link #addResponderPlugin(RSocketInterceptor)} instead */ + @Deprecated public ServerRSocketFactory addServerPlugin(RSocketInterceptor interceptor) { - plugins.addServerPlugin(interceptor); + return addResponderPlugin(interceptor); + } + + public ServerRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { + plugins.addResponderPlugin(interceptor); return this; } @@ -525,29 +547,29 @@ private Mono acceptSetup( (keepAliveHandler, wrappedMultiplexer) -> { ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(setupFrame); - RSocketClient rSocketClient = - new RSocketClient( + RSocketRequester rSocketRequester = + new RSocketRequester( allocator, wrappedMultiplexer.asServerConnection(), payloadDecoder, errorConsumer, StreamIdSupplier.serverSupplier()); - RSocket wrappedRSocketClient = plugins.applyClient(rSocketClient); + RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); return acceptor - .accept(setupPayload, wrappedRSocketClient) + .accept(setupPayload, wrappedRSocketRequester) .onErrorResume( err -> sendError(multiplexer, rejectedSetupError(err)).then(Mono.error(err))) .doOnNext( - unwrappedServerSocket -> { - RSocket wrappedRSocketServer = plugins.applyServer(unwrappedServerSocket); + rSocketHandler -> { + RSocket wrappedRSocketHandler = plugins.applyResponder(rSocketHandler); - RSocketServer rSocketServer = - new RSocketServer( + RSocketResponder rSocketResponder = + new RSocketResponder( allocator, wrappedMultiplexer.asClientConnection(), - wrappedRSocketServer, + wrappedRSocketHandler, payloadDecoder, errorConsumer, setupPayload.keepAliveInterval(), diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java similarity index 99% rename from rsocket-core/src/main/java/io/rsocket/RSocketClient.java rename to rsocket-core/src/main/java/io/rsocket/RSocketRequester.java index 539e104b4..9c01db295 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketClient.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java @@ -45,8 +45,10 @@ import org.reactivestreams.Subscriber; import reactor.core.publisher.*; -/** Client Side of a RSocket socket. Sends {@link ByteBuf}s to a {@link RSocketServer} */ -class RSocketClient implements RSocket { +/** + * Requester Side of a RSocket socket. Sends {@link ByteBuf}s to a {@link RSocketResponder} of peer + */ +class RSocketRequester implements RSocket { private final DuplexConnection connection; private final PayloadDecoder payloadDecoder; @@ -60,7 +62,7 @@ class RSocketClient implements RSocket { private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; /*client requester*/ - RSocketClient( + RSocketRequester( ByteBufAllocator allocator, DuplexConnection connection, PayloadDecoder payloadDecoder, @@ -99,7 +101,7 @@ class RSocketClient implements RSocket { } /*server requester*/ - RSocketClient( + RSocketRequester( ByteBufAllocator allocator, DuplexConnection connection, PayloadDecoder payloadDecoder, diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java similarity index 98% rename from rsocket-core/src/main/java/io/rsocket/RSocketServer.java rename to rsocket-core/src/main/java/io/rsocket/RSocketResponder.java index 6ce5ef88d..f861303a4 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java @@ -43,8 +43,8 @@ import reactor.core.Exceptions; import reactor.core.publisher.*; -/** Server side RSocket. Receives {@link ByteBuf}s from a {@link RSocketClient} */ -class RSocketServer implements ResponderRSocket { +/** Responder side of RSocket. Receives {@link ByteBuf}s from a peer's {@link RSocketRequester} */ +class RSocketResponder implements ResponderRSocket { private final DuplexConnection connection; private final RSocket requestHandler; @@ -61,7 +61,7 @@ class RSocketServer implements ResponderRSocket { private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; /*client responder*/ - RSocketServer( + RSocketResponder( ByteBufAllocator allocator, DuplexConnection connection, RSocket requestHandler, @@ -71,7 +71,7 @@ class RSocketServer implements ResponderRSocket { } /*server responder*/ - RSocketServer( + RSocketResponder( ByteBufAllocator allocator, DuplexConnection connection, RSocket requestHandler, diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java index 873f6babb..676cfc19c 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java @@ -23,39 +23,63 @@ public class PluginRegistry { private List connections = new ArrayList<>(); - private List clients = new ArrayList<>(); - private List servers = new ArrayList<>(); + private List requesters = new ArrayList<>(); + private List responders = new ArrayList<>(); public PluginRegistry() {} public PluginRegistry(PluginRegistry defaults) { this.connections.addAll(defaults.connections); - this.clients.addAll(defaults.clients); - this.servers.addAll(defaults.servers); + this.requesters.addAll(defaults.requesters); + this.responders.addAll(defaults.responders); } public void addConnectionPlugin(DuplexConnectionInterceptor interceptor) { connections.add(interceptor); } + /** Deprecated. Use {@link #addRequesterPlugin(RSocketInterceptor)} instead */ + @Deprecated public void addClientPlugin(RSocketInterceptor interceptor) { - clients.add(interceptor); + addRequesterPlugin(interceptor); } + public void addRequesterPlugin(RSocketInterceptor interceptor) { + requesters.add(interceptor); + } + + /** Deprecated. Use {@link #addResponderPlugin(RSocketInterceptor)} instead */ + @Deprecated public void addServerPlugin(RSocketInterceptor interceptor) { - servers.add(interceptor); + addResponderPlugin(interceptor); + } + + public void addResponderPlugin(RSocketInterceptor interceptor) { + responders.add(interceptor); } + /** Deprecated. Use {@link #applyRequester(RSocket)} instead */ + @Deprecated public RSocket applyClient(RSocket rSocket) { - for (RSocketInterceptor i : clients) { + return applyRequester(rSocket); + } + + public RSocket applyRequester(RSocket rSocket) { + for (RSocketInterceptor i : requesters) { rSocket = i.apply(rSocket); } return rSocket; } + /** Deprecated. Use {@link #applyResponder(RSocket)} instead */ + @Deprecated public RSocket applyServer(RSocket rSocket) { - for (RSocketInterceptor i : servers) { + return applyResponder(rSocket); + } + + public RSocket applyResponder(RSocket rSocket) { + for (RSocketInterceptor i : responders) { rSocket = i.apply(rSocket); } diff --git a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java index aededb804..106e61097 100644 --- a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java @@ -65,8 +65,8 @@ static Supplier requester(int tickPeriod, int timeout) { return () -> { TestDuplexConnection connection = new TestDuplexConnection(); Errors errors = new Errors(); - RSocketClient rSocket = - new RSocketClient( + RSocketRequester rSocket = + new RSocketRequester( ByteBufAllocator.DEFAULT, connection, DefaultPayload::create, @@ -84,8 +84,8 @@ static Supplier responder(int tickPeriod, int timeout) { TestDuplexConnection connection = new TestDuplexConnection(); AbstractRSocket handler = new AbstractRSocket() {}; Errors errors = new Errors(); - RSocketServer rSocket = - new RSocketServer( + RSocketResponder rSocket = + new RSocketResponder( ByteBufAllocator.DEFAULT, connection, handler, @@ -110,8 +110,8 @@ static Supplier resumableRequester(int tickPeriod, int ti false); Errors errors = new Errors(); - RSocketClient rSocket = - new RSocketClient( + RSocketRequester rSocket = + new RSocketRequester( ByteBufAllocator.DEFAULT, resumableConnection, DefaultPayload::create, @@ -136,8 +136,8 @@ static Supplier resumableResponder(int tickPeriod, int ti Duration.ofSeconds(10), false); Errors errors = new Errors(); - RSocketServer rSocket = - new RSocketServer( + RSocketResponder rSocket = + new RSocketResponder( ByteBufAllocator.DEFAULT, resumableConnection, handler, diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketClientTerminationTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketRequesterTerminationTest.java similarity index 86% rename from rsocket-core/src/test/java/io/rsocket/RSocketClientTerminationTest.java rename to rsocket-core/src/test/java/io/rsocket/RSocketRequesterTerminationTest.java index ae3bfc489..a2c17cf95 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketClientTerminationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketRequesterTerminationTest.java @@ -1,6 +1,6 @@ package io.rsocket; -import io.rsocket.RSocketClientTest.ClientSocketRule; +import io.rsocket.RSocketRequesterTest.ClientSocketRule; import io.rsocket.util.EmptyPayload; import java.nio.channels.ClosedChannelException; import java.time.Duration; @@ -16,18 +16,18 @@ import reactor.test.StepVerifier; @RunWith(Parameterized.class) -public class RSocketClientTerminationTest { +public class RSocketRequesterTerminationTest { @Rule public final ClientSocketRule rule = new ClientSocketRule(); private Function> interaction; - public RSocketClientTerminationTest(Function> interaction) { + public RSocketRequesterTerminationTest(Function> interaction) { this.interaction = interaction; } @Test public void testCurrentStreamIsTerminatedOnConnectionClose() { - RSocketClient rSocket = rule.socket; + RSocketRequester rSocket = rule.socket; Mono.delay(Duration.ofSeconds(1)).doOnNext(v -> rule.connection.dispose()).subscribe(); @@ -38,7 +38,7 @@ public void testCurrentStreamIsTerminatedOnConnectionClose() { @Test public void testSubsequentStreamIsTerminatedAfterConnectionClose() { - RSocketClient rSocket = rule.socket; + RSocketRequester rSocket = rule.socket; rule.connection.dispose(); StepVerifier.create(interaction.apply(rSocket)) diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketRequesterTest.java similarity index 98% rename from rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java rename to rsocket-core/src/test/java/io/rsocket/RSocketRequesterTest.java index c60dba312..7337161e0 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketClientTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketRequesterTest.java @@ -47,7 +47,7 @@ import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.UnicastProcessor; -public class RSocketClientTest { +public class RSocketRequesterTest { @Rule public final ClientSocketRule rule = new ClientSocketRule(); @@ -223,10 +223,10 @@ public int sendRequestResponse(Publisher response) { return streamId; } - public static class ClientSocketRule extends AbstractSocketRule { + public static class ClientSocketRule extends AbstractSocketRule { @Override - protected RSocketClient newRSocket() { - return new RSocketClient( + protected RSocketRequester newRSocket() { + return new RSocketRequester( ByteBufAllocator.DEFAULT, connection, DefaultPayload::create, diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketResponderTest.java similarity index 97% rename from rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java rename to rsocket-core/src/test/java/io/rsocket/RSocketResponderTest.java index 32c0406b9..c14a73e69 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketServerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketResponderTest.java @@ -37,7 +37,7 @@ import org.reactivestreams.Subscriber; import reactor.core.publisher.Mono; -public class RSocketServerTest { +public class RSocketResponderTest { @Rule public final ServerSocketRule rule = new ServerSocketRule(); @@ -106,7 +106,7 @@ public Mono requestResponse(Payload payload) { assertThat("Subscription not cancelled.", cancelled.get(), is(true)); } - public static class ServerSocketRule extends AbstractSocketRule { + public static class ServerSocketRule extends AbstractSocketRule { private RSocket acceptingSocket; @@ -140,8 +140,8 @@ public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { } @Override - protected RSocketServer newRSocket() { - return new RSocketServer( + protected RSocketResponder newRSocket() { + return new RSocketResponder( ByteBufAllocator.DEFAULT, connection, acceptingSocket, diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketTest.java index 9ef37a398..4a7ad45ef 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketTest.java @@ -102,10 +102,10 @@ public static class SocketRule extends ExternalResource { DirectProcessor serverProcessor; DirectProcessor clientProcessor; - private RSocketClient crs; + private RSocketRequester crs; @SuppressWarnings("unused") - private RSocketServer srs; + private RSocketResponder srs; private RSocket requestAcceptor; private ArrayList clientErrors = new ArrayList<>(); @@ -163,7 +163,7 @@ public Flux requestChannel(Publisher payloads) { }; srs = - new RSocketServer( + new RSocketResponder( ByteBufAllocator.DEFAULT, serverConnection, requestAcceptor, @@ -171,7 +171,7 @@ public Flux requestChannel(Publisher payloads) { throwable -> serverErrors.add(throwable)); crs = - new RSocketClient( + new RSocketRequester( ByteBufAllocator.DEFAULT, clientConnection, DefaultPayload::create, diff --git a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java index 16b7eafb5..74bbe083c 100644 --- a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java @@ -49,8 +49,8 @@ void responderRejectSetup() { void requesterStreamsTerminatedOnZeroErrorFrame() { TestDuplexConnection conn = new TestDuplexConnection(); List errors = new ArrayList<>(); - RSocketClient rSocket = - new RSocketClient( + RSocketRequester rSocket = + new RSocketRequester( ByteBufAllocator.DEFAULT, conn, DefaultPayload::create, @@ -79,8 +79,8 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { @Test void requesterNewStreamsTerminatedAfterZeroErrorFrame() { TestDuplexConnection conn = new TestDuplexConnection(); - RSocketClient rSocket = - new RSocketClient( + RSocketRequester rSocket = + new RSocketRequester( ByteBufAllocator.DEFAULT, conn, DefaultPayload::create, From c0c3e90bc69f273b381f5342369a1652e9548358 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Fri, 31 May 2019 11:54:56 +0300 Subject: [PATCH 050/181] Fix leaks and issues related to fragmentation (#645) * fragmentation: server closeable Mono signals error if mtu is not sufficient client closeable Mono signals error if mtu is not sufficient, before opening a connection Signed-off-by: Maksym Ostroverkhov * fix memory leaks on frame reassembly Signed-off-by: Maksym Ostroverkhov * fix memory leak on frame logging in ClientServerInputMultiplexer Signed-off-by: Maksym Ostroverkhov * rename RSocketFactory netty transport integration test Signed-off-by: Maksym Ostroverkhov * fix stream and channel response having empty data on some (payload size/fragmentation mtu) combinations Signed-off-by: Maksym Ostroverkhov * remove debug code Signed-off-by: Maksym Ostroverkhov * workaround javac 11.0.3 issue Signed-off-by: Maksym Ostroverkhov --- .../FragmentationDuplexConnection.java | 42 ++++-- .../fragmentation/FrameReassembler.java | 18 +-- .../main/java/io/rsocket/frame/FrameUtil.java | 2 +- .../FragmentationDuplexConnectionTest.java | 4 +- .../transport/local/LocalClientTransport.java | 3 +- .../transport/local/LocalServerTransport.java | 25 ++-- .../netty/client/TcpClientTransport.java | 35 ++--- .../client/WebsocketClientTransport.java | 33 ++--- .../netty/server/TcpServerTransport.java | 47 ++++--- .../server/WebsocketServerTransport.java | 42 +++--- ...actoryNettyTransportFragmentationTest.java | 125 ++++++++++++++++++ 11 files changed, 270 insertions(+), 106 deletions(-) create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/RSocketFactoryNettyTransportFragmentationTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java index 68e15fe70..cbe989d4b 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java @@ -26,6 +26,7 @@ import io.rsocket.frame.FrameLengthFlyweight; import io.rsocket.frame.FrameType; import java.util.Objects; +import javax.annotation.Nullable; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,13 +58,10 @@ public FragmentationDuplexConnection( String type) { Objects.requireNonNull(delegate, "delegate must not be null"); Objects.requireNonNull(allocator, "byteBufAllocator must not be null"); - if (mtu < MIN_MTU_SIZE) { - throw new IllegalArgumentException("smallest allowed mtu size is " + MIN_MTU_SIZE + " bytes"); - } this.encodeLength = encodeLength; this.allocator = allocator; this.delegate = delegate; - this.mtu = mtu; + this.mtu = assertMtu(mtu); this.frameReassembler = new FrameReassembler(allocator); this.type = type; @@ -74,6 +72,32 @@ private boolean shouldFragment(FrameType frameType, int readableBytes) { return frameType.isFragmentable() && readableBytes > mtu; } + /*TODO this is nullable and not returning empty to workaround javac 11.0.3 compiler issue on ubuntu (at least) */ + @Nullable + public static Mono checkMtu(int mtu) { + if (isInsufficientMtu(mtu)) { + String msg = + String.format("smallest allowed mtu size is %d bytes, provided: %d", MIN_MTU_SIZE, mtu); + return Mono.error(new IllegalArgumentException(msg)); + } else { + return null; + } + } + + private static int assertMtu(int mtu) { + if (isInsufficientMtu(mtu)) { + String msg = + String.format("smallest allowed mtu size is %d bytes, provided: %d", MIN_MTU_SIZE, mtu); + throw new IllegalArgumentException(msg); + } else { + return mtu; + } + } + + private static boolean isInsufficientMtu(int mtu) { + return mtu > 0 && mtu < MIN_MTU_SIZE || mtu < 0; + } + @Override public Mono send(Publisher frames) { return Flux.from(frames).concatMap(this::sendOne).then(); @@ -89,13 +113,13 @@ public Mono sendOne(ByteBuf frame) { Flux.from(fragmentFrame(allocator, mtu, frame, frameType, encodeLength)) .doOnNext( byteBuf -> { - ByteBuf frame1 = FrameLengthFlyweight.frame(byteBuf); + ByteBuf f = encodeLength ? FrameLengthFlyweight.frame(byteBuf) : byteBuf; logger.debug( "{} - stream id {} - frame type {} - \n {}", type, - FrameHeaderFlyweight.streamId(frame1), - FrameHeaderFlyweight.frameType(frame1), - ByteBufUtil.prettyHexDump(frame1)); + FrameHeaderFlyweight.streamId(f), + FrameHeaderFlyweight.frameType(f), + ByteBufUtil.prettyHexDump(f)); })); } else { return delegate.send( @@ -108,7 +132,7 @@ public Mono sendOne(ByteBuf frame) { private ByteBuf encode(ByteBuf frame) { if (encodeLength) { - return FrameLengthFlyweight.encode(allocator, frame.readableBytes(), frame).retain(); + return FrameLengthFlyweight.encode(allocator, frame.readableBytes(), frame); } else { return frame; } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java index 4f65ebf63..5660e3615 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java @@ -151,6 +151,7 @@ void handleNoFollowsFlag(ByteBuf frame, SynchronousSink sink, int strea ByteBuf assembledFrame = FragmentationFlyweight.encode(allocator, header, data); sink.next(assembledFrame); } + frame.release(); } else { sink.next(frame); } @@ -220,9 +221,8 @@ void handleFollowsFlag(ByteBuf frame, int streamId, FrameType frameType) { throw new IllegalStateException("unsupported fragment type"); } - if (data != Unpooled.EMPTY_BUFFER) { - getData(streamId).addComponents(true, data); - } + getData(streamId).addComponents(true, data); + frame.release(); } void reassembleFrame(ByteBuf frame, SynchronousSink sink) { @@ -259,25 +259,21 @@ private ByteBuf assembleFrameWithMetadata(ByteBuf frame, int streamId, ByteBuf h ByteBuf metadata; CompositeByteBuf cm = removeMetadata(streamId); if (cm != null) { - ByteBuf m = PayloadFrameFlyweight.metadata(frame); - metadata = cm.addComponents(true, m); + metadata = cm.addComponents(true, PayloadFrameFlyweight.metadata(frame).retain()); } else { - metadata = PayloadFrameFlyweight.metadata(frame); + metadata = PayloadFrameFlyweight.metadata(frame).retain(); } ByteBuf data = assembleData(frame, streamId); - return FragmentationFlyweight.encode(allocator, header, metadata.retain(), data); + return FragmentationFlyweight.encode(allocator, header, metadata, data); } private ByteBuf assembleData(ByteBuf frame, int streamId) { ByteBuf data; CompositeByteBuf cd = removeData(streamId); if (cd != null) { - ByteBuf d = PayloadFrameFlyweight.data(frame); - if (d != null) { - cd.addComponents(true, d); - } + cd.addComponents(true, PayloadFrameFlyweight.data(frame).retain()); data = cd; } else { data = Unpooled.EMPTY_BUFFER; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java index 417e44857..a4d862135 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java @@ -70,7 +70,7 @@ private static ByteBuf getMetadata(ByteBuf frame, FrameType frameType) { default: return Unpooled.EMPTY_BUFFER; } - return metadata.retain(); + return metadata; } else { return Unpooled.EMPTY_BUFFER; } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java index a16f6d28d..3d96bfd12 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java @@ -64,7 +64,7 @@ void constructorInvalidMaxFragmentSize() { () -> new FragmentationDuplexConnection( delegate, allocator, Integer.MIN_VALUE, false, "")) - .withMessage("smallest allowed mtu size is 64 bytes"); + .withMessage("smallest allowed mtu size is 64 bytes, provided: -2147483648"); } @DisplayName("constructor throws IllegalArgumentException with negative maxFragmentLength") @@ -72,7 +72,7 @@ void constructorInvalidMaxFragmentSize() { void constructorMtuLessThanMin() { assertThatIllegalArgumentException() .isThrownBy(() -> new FragmentationDuplexConnection(delegate, allocator, 2, false, "")) - .withMessage("smallest allowed mtu size is 64 bytes"); + .withMessage("smallest allowed mtu size is 64 bytes, provided: 2"); } @DisplayName("constructor throws NullPointerException with null byteBufAllocator") diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java index e41d9e7db..40db8ef74 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java @@ -76,7 +76,8 @@ private Mono connect() { @Override public Mono connect(int mtu) { - Mono connect = connect(); + Mono isError = FragmentationDuplexConnection.checkMtu(mtu); + Mono connect = isError != null ? isError : connect(); if (mtu > 0) { return connect.map( duplexConnection -> diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java index f649d30ce..d755859d2 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java @@ -107,17 +107,20 @@ public LocalClientTransport clientTransport() { public Mono start(ConnectionAcceptor acceptor, int mtu) { Objects.requireNonNull(acceptor, "acceptor must not be null"); - return Mono.create( - sink -> { - ServerDuplexConnectionAcceptor serverDuplexConnectionAcceptor = - new ServerDuplexConnectionAcceptor(name, acceptor, mtu); - - if (registry.putIfAbsent(name, serverDuplexConnectionAcceptor) != null) { - throw new IllegalStateException("name already registered: " + name); - } - - sink.success(serverDuplexConnectionAcceptor); - }); + Mono isError = FragmentationDuplexConnection.checkMtu(mtu); + return isError != null + ? isError + : Mono.create( + sink -> { + ServerDuplexConnectionAcceptor serverDuplexConnectionAcceptor = + new ServerDuplexConnectionAcceptor(name, acceptor, mtu); + + if (registry.putIfAbsent(name, serverDuplexConnectionAcceptor) != null) { + throw new IllegalStateException("name already registered: " + name); + } + + sink.success(serverDuplexConnectionAcceptor); + }); } /** diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java index 250d2eee0..f5e79e9bf 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java @@ -94,21 +94,24 @@ public static TcpClientTransport create(TcpClient client) { @Override public Mono connect(int mtu) { - return client - .doOnConnected(c -> c.addHandlerLast(new RSocketLengthCodec())) - .connect() - .map( - c -> { - if (mtu > 0) { - return new FragmentationDuplexConnection( - new TcpDuplexConnection(c, false), - ByteBufAllocator.DEFAULT, - mtu, - true, - "client"); - } else { - return new TcpDuplexConnection(c); - } - }); + Mono isError = FragmentationDuplexConnection.checkMtu(mtu); + return isError != null + ? isError + : client + .doOnConnected(c -> c.addHandlerLast(new RSocketLengthCodec())) + .connect() + .map( + c -> { + if (mtu > 0) { + return new FragmentationDuplexConnection( + new TcpDuplexConnection(c, false), + ByteBufAllocator.DEFAULT, + mtu, + true, + "client"); + } else { + return new TcpDuplexConnection(c); + } + }); } } 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 0da5b04d8..5049119a5 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 @@ -151,21 +151,24 @@ private static TcpClient createClient(URI uri) { @Override public Mono connect(int mtu) { - return client - .headers(headers -> transportHeaders.get().forEach(headers::set)) - .websocket(FRAME_LENGTH_MASK) - .uri(path) - .connect() - .map( - c -> { - DuplexConnection connection = new WebsocketDuplexConnection(c); - if (mtu > 0) { - connection = - new FragmentationDuplexConnection( - connection, ByteBufAllocator.DEFAULT, mtu, false, "client"); - } - return connection; - }); + Mono isError = FragmentationDuplexConnection.checkMtu(mtu); + return isError != null + ? isError + : client + .headers(headers -> transportHeaders.get().forEach(headers::set)) + .websocket(FRAME_LENGTH_MASK) + .uri(path) + .connect() + .map( + c -> { + DuplexConnection connection = new WebsocketDuplexConnection(c); + if (mtu > 0) { + connection = + new FragmentationDuplexConnection( + connection, ByteBufAllocator.DEFAULT, mtu, false, "client"); + } + return connection; + }); } @Override diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java index b7f60aa6c..54ef016c0 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java @@ -94,26 +94,31 @@ public static TcpServerTransport create(TcpServer server) { @Override public Mono start(ConnectionAcceptor acceptor, int mtu) { Objects.requireNonNull(acceptor, "acceptor must not be null"); - - return server - .doOnConnection( - c -> { - c.addHandlerLast(new RSocketLengthCodec()); - DuplexConnection connection; - if (mtu > 0) { - connection = - new FragmentationDuplexConnection( - new TcpDuplexConnection(c, false), - ByteBufAllocator.DEFAULT, - mtu, - true, - "server"); - } else { - connection = new TcpDuplexConnection(c); - } - acceptor.apply(connection).then(Mono.never()).subscribe(c.disposeSubscriber()); - }) - .bind() - .map(CloseableChannel::new); + Mono isError = FragmentationDuplexConnection.checkMtu(mtu); + return isError != null + ? isError + : server + .doOnConnection( + c -> { + c.addHandlerLast(new RSocketLengthCodec()); + DuplexConnection connection; + if (mtu > 0) { + connection = + new FragmentationDuplexConnection( + new TcpDuplexConnection(c, false), + ByteBufAllocator.DEFAULT, + mtu, + true, + "server"); + } else { + connection = new TcpDuplexConnection(c); + } + acceptor + .apply(connection) + .then(Mono.never()) + .subscribe(c.disposeSubscriber()); + }) + .bind() + .map(CloseableChannel::new); } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java index 4ac68cdfd..ee0b8e3d3 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java @@ -111,24 +111,28 @@ public void setTransportHeaders(Supplier> transportHeaders) public Mono start(ConnectionAcceptor acceptor, int mtu) { Objects.requireNonNull(acceptor, "acceptor must not be null"); - return server - .handle( - (request, response) -> { - transportHeaders.get().forEach(response::addHeader); - return response.sendWebsocket( - null, - FRAME_LENGTH_MASK, - (in, out) -> { - DuplexConnection connection = new WebsocketDuplexConnection((Connection) in); - if (mtu > 0) { - connection = - new FragmentationDuplexConnection( - connection, ByteBufAllocator.DEFAULT, mtu, false, "server"); - } - return acceptor.apply(connection).then(out.neverComplete()); - }); - }) - .bind() - .map(CloseableChannel::new); + Mono isError = FragmentationDuplexConnection.checkMtu(mtu); + return isError != null + ? isError + : server + .handle( + (request, response) -> { + transportHeaders.get().forEach(response::addHeader); + return response.sendWebsocket( + null, + FRAME_LENGTH_MASK, + (in, out) -> { + DuplexConnection connection = + new WebsocketDuplexConnection((Connection) in); + if (mtu > 0) { + connection = + new FragmentationDuplexConnection( + connection, ByteBufAllocator.DEFAULT, mtu, false, "server"); + } + return acceptor.apply(connection).then(out.neverComplete()); + }); + }) + .bind() + .map(CloseableChannel::new); } } diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/RSocketFactoryNettyTransportFragmentationTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/RSocketFactoryNettyTransportFragmentationTest.java new file mode 100644 index 000000000..07e9378fa --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/RSocketFactoryNettyTransportFragmentationTest.java @@ -0,0 +1,125 @@ +package io.rsocket.transport.netty; + +import static io.rsocket.RSocketFactory.*; + +import io.rsocket.RSocket; +import io.rsocket.RSocketFactory; +import io.rsocket.SocketAcceptor; +import io.rsocket.transport.ClientTransport; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.CloseableChannel; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.transport.netty.server.WebsocketServerTransport; +import java.time.Duration; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class RSocketFactoryNettyTransportFragmentationTest { + + @ParameterizedTest + @MethodSource("serverTransportProvider") + void serverErrorsWithEnabledFragmentationOnInsufficientMtu( + ServerTransport serverTransport) { + Mono server = createServer(serverTransport, f -> f.fragment(2)); + + StepVerifier.create(server) + .expectErrorMatches( + err -> + err instanceof IllegalArgumentException + && "smallest allowed mtu size is 64 bytes, provided: 2" + .equals(err.getMessage())) + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("serverTransportProvider") + void serverSucceedsWithEnabledFragmentationOnSufficientMtu( + ServerTransport serverTransport) { + Mono server = + createServer(serverTransport, f -> f.fragment(100)).doOnNext(CloseableChannel::dispose); + StepVerifier.create(server).expectNextCount(1).expectComplete().verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("serverTransportProvider") + void serverSucceedsWithDisabledFragmentation() { + Mono server = + createServer(TcpServerTransport.create("localhost", 0), Function.identity()) + .doOnNext(CloseableChannel::dispose); + StepVerifier.create(server).expectNextCount(1).expectComplete().verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("serverTransportProvider") + void clientErrorsWithEnabledFragmentationOnInsufficientMtu( + ServerTransport serverTransport) { + CloseableChannel server = createServer(serverTransport, f -> f.fragment(100)).block(); + + Mono rSocket = + createClient(TcpClientTransport.create(server.address()), f -> f.fragment(2)) + .doFinally(s -> server.dispose()); + + StepVerifier.create(rSocket) + .expectErrorMatches( + err -> + err instanceof IllegalArgumentException + && "smallest allowed mtu size is 64 bytes, provided: 2" + .equals(err.getMessage())) + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("serverTransportProvider") + void clientSucceedsWithEnabledFragmentationOnSufficientMtu( + ServerTransport serverTransport) { + CloseableChannel server = createServer(serverTransport, f -> f.fragment(100)).block(); + + Mono rSocket = + createClient(TcpClientTransport.create(server.address()), f -> f.fragment(100)) + .doFinally(s -> server.dispose()); + StepVerifier.create(rSocket).expectNextCount(1).expectComplete().verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("serverTransportProvider") + void clientSucceedsWithDisabledFragmentation() { + CloseableChannel server = + createServer(TcpServerTransport.create("localhost", 0), Function.identity()).block(); + + Mono rSocket = + createClient(TcpClientTransport.create(server.address()), Function.identity()) + .doFinally(s -> server.dispose()); + StepVerifier.create(rSocket).expectNextCount(1).expectComplete().verify(Duration.ofSeconds(5)); + } + + private Mono createClient( + ClientTransport transport, Function f) { + return f.apply(RSocketFactory.connect()).transport(transport).start(); + } + + private Mono createServer( + ServerTransport transport, + Function f) { + return f.apply(receive()).acceptor(mockAcceptor()).transport(transport).start(); + } + + private SocketAcceptor mockAcceptor() { + SocketAcceptor mock = Mockito.mock(SocketAcceptor.class); + Mockito.when(mock.accept(Mockito.any(), Mockito.any())) + .thenReturn(Mono.just(Mockito.mock(RSocket.class))); + return mock; + } + + static Stream> serverTransportProvider() { + String host = "localhost"; + int port = 0; + return Stream.of( + TcpServerTransport.create(host, port), WebsocketServerTransport.create(host, port)); + } +} From c176054a284cf6e9fc49d8d53d79bc4bfbf9ce1b Mon Sep 17 00:00:00 2001 From: Stephane Maldini Date: Wed, 5 Jun 2019 14:35:22 -0700 Subject: [PATCH 051/181] Travis icon should point to develop, not 1.0.x --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5eacd94f0..53158ea04 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Learn more at http://rsocket.io ## Build and Binaries -[![Build Status](https://travis-ci.org/rsocket/rsocket-java.svg?branch=1.0.x)](https://travis-ci.org/rsocket/rsocket-java) +[![Build Status](https://travis-ci.org/rsocket/rsocket-java.svg?branch=develop)](https://travis-ci.org/rsocket/rsocket-java) Releases are available via Maven Central. From 8b8dba15baa73141a4ca71c7374632d0dd3298a7 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 6 Jun 2019 09:41:05 +0200 Subject: [PATCH 052/181] headers treating sample (#646) Signed-off-by: Oleh Dokuka --- .../transport/ws/WebSocketHeadersSample.java | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketHeadersSample.java 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 new file mode 100644 index 000000000..d3865c01b --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/ws/WebSocketHeadersSample.java @@ -0,0 +1,141 @@ +/* + * Copyright 2015-2018 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.ws; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.rsocket.AbstractRSocket; +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.DuplexConnection; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.RSocketFactory; +import io.rsocket.SocketAcceptor; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.WebsocketDuplexConnection; +import io.rsocket.transport.netty.client.WebsocketClientTransport; +import io.rsocket.util.ByteBufPayload; +import java.time.Duration; +import java.util.HashMap; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.netty.Connection; +import reactor.netty.DisposableServer; +import reactor.netty.http.server.HttpServer; + +public class WebSocketHeadersSample { + static final Payload payload1 = ByteBufPayload.create("Hello "); + + public static void main(String[] args) { + + ServerTransport.ConnectionAcceptor acceptor = + RSocketFactory.receive() + .frameDecoder(PayloadDecoder.ZERO_COPY) + .acceptor(new SocketAcceptorImpl()) + .toConnectionAcceptor(); + + DisposableServer disposableServer = + HttpServer.create() + .host("localhost") + .port(0) + .route( + routes -> + routes.ws( + "/", + (in, out) -> { + if (in.headers().containsValue("Authorization", "test", true)) { + DuplexConnection connection = + new WebsocketDuplexConnection((Connection) in); + return acceptor.apply(connection).then(out.neverComplete()); + } + + return out.sendClose( + HttpResponseStatus.UNAUTHORIZED.code(), + HttpResponseStatus.UNAUTHORIZED.reasonPhrase()); + })) + .bindNow(); + + WebsocketClientTransport clientTransport = + WebsocketClientTransport.create(disposableServer.host(), disposableServer.port()); + + clientTransport.setTransportHeaders( + () -> { + HashMap map = new HashMap<>(); + map.put("Authorization", "test"); + return map; + }); + + RSocket socket = + RSocketFactory.connect() + .keepAliveAckTimeout(Duration.ofMinutes(10)) + .frameDecoder(PayloadDecoder.ZERO_COPY) + .transport(clientTransport) + .start() + .block(); + + Flux.range(0, 100) + .concatMap(i -> socket.fireAndForget(payload1.retain())) + // .doOnNext(p -> { + //// System.out.println(p.getDataUtf8()); + // p.release(); + // }) + .blockLast(); + socket.dispose(); + + WebsocketClientTransport clientTransport2 = + WebsocketClientTransport.create(disposableServer.host(), disposableServer.port()); + + RSocket rSocket = + RSocketFactory.connect() + .keepAliveAckTimeout(Duration.ofMinutes(10)) + .frameDecoder(PayloadDecoder.ZERO_COPY) + .transport(clientTransport2) + .start() + .block(); + + // expect error here because of closed channel + rSocket.requestResponse(payload1).block(); + } + + private static class SocketAcceptorImpl implements SocketAcceptor { + @Override + public Mono accept(ConnectionSetupPayload setupPayload, RSocket reactiveSocket) { + return Mono.just( + new AbstractRSocket() { + + @Override + public Mono fireAndForget(Payload payload) { + // System.out.println(payload.getDataUtf8()); + payload.release(); + return Mono.empty(); + } + + @Override + public Mono requestResponse(Payload payload) { + return Mono.just(payload); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads).subscribeOn(Schedulers.single()); + } + }); + } + } +} From 22e65038aeb5907f798f851ab99f308cbdb1e2c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Tue, 11 Jun 2019 13:27:02 -0400 Subject: [PATCH 053/181] Well known mime types and Composite metadata extension (#635) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add encodeUnsignedMedium to NumberUtils Signed-off-by: Simon Baslé * Add WellKnownMimeType enum with String/ID conversions Signed-off-by: Simon Baslé * Add a flyweight capable of encoding Composite Metadata The flyweight also exposes a method to decode Composite Metadata to a Map. Signed-off-by: Simon Baslé * Move flyweight into metadata package, change decodeToMap to decodeNext Signed-off-by: Simon Baslé * Add CompositeMetadata interface with factory methods to decode/encode The API is intended as a readonly way of listing the composite metadata mime-buffer pairs, as well as accessing metadata pairs by mime type or index, including the case where a mime type is associated with several metadata entries (`List getAll(String)`)... Signed-off-by: Simon Baslé * Add first test for CompositeMetadata (decode smoke test) Signed-off-by: Simon Baslé * Expose incremental decode method that decodes a single metadata entry Signed-off-by: Simon Baslé * WellKnownMimeType parsing now avoids exceptions (except null arguments) Instead has 2 special enums. int parsing allows detection of potentially legit type that is still unknown to the current implementation, which could help making it more future-proof. Signed-off-by: Simon Baslé * rework encoding / decoding API around Entry, accommodate 3 mime case Encoding becomes a mirror of decoding, purely based on Entry. Accommodate 3 flavors of mime type decoding: - compressed well known with id matching an enum - uncompressed custom string - compressed but known as reserved for future use (future proofness) The later can be decoded into a special Entry that will allow re encoding it and transmitting it to other nodes as it was. Signed-off-by: Simon Baslé * No need for a CompositeMetadata interface, single concrete implem Signed-off-by: Simon Baslé * Expose the CompositeMetadataFlyweight for low-level, simplify API - the flyweight deals with low-level encoding/decoding with minimal garbage and only uses exceptions in the encoding path -- encoding a non-ASCII custom mime type OR -- encoding an out of range byte - the CompositeMetadata is an _option_ for a higher level Iterable-based API, but which instantiates new types Signed-off-by: Simon Baslé * Remove CompositeMetadata, make Entry child of flyweight Entry is now the only high-level abstraction, will need a way to decode into a List. Signed-off-by: Simon Baslé * Add javadoc for CompositeMetadataFlyweight methods Signed-off-by: Simon Baslé * Switch to a single Entry implementation, remove isPassthrough Signed-off-by: Simon Baslé * Rename Entry#decodeEntry to #decode, add #decodeAll Signed-off-by: Simon Baslé * apply Google-style formatting Signed-off-by: Simon Baslé * Use precomputed array for WellKnownMimeType.fromId Signed-off-by: Simon Baslé * Use precomputed Map for fromMimeType + jmh benchmark to validate perf increase Signed-off-by: Simon Baslé * Several custom mime metadata header encoding improvements: - avoid the use of a CompositeByteBuf - length can be safely predicted due to requirement that custom mime type be ASCII only - still using UTF8 writing and ByteBufUtil.isText(ASCII) to detect non-ascii text as: - writeAscii would not reject non-ascii chars but replace with `?` - ISO-8859 chars (eg. Latin-1) could be considered valid when only checking the number of written bytes - use of ByteBufUtil.writeUtf8 to simplify encoding the mime into the preallocated buffer - release the buffer if there is an encoding exception Signed-off-by: Simon Baslé * Use Unpooled buffers in tests Signed-off-by: Simon Baslé * Remove Entry, replace with CompositeMetadata Iterator-like abstraction The CompositeMetadata wraps a ByteBuf and exposes a hasNext/decodeNext API (reminiscent of the Iterator API) as a facade over the flyweight's methods. Each round decodeNext() is correctly invoked, the CompositeMetadata state is updated and the getters can be used to retrieve decoded entry components. Signed-off-by: Simon Baslé * Add an encode flyweight method that attempts compressing String This is a qol method that will first check if the String passed can be mapped to a WellKnownMimeType (and thus compressed to a 1 byte representation). Signed-off-by: Simon Baslé * Avoid moving full buffer's readerIndex when slicing entries This implies that the index from which to decode and slice the next entry MUST be provided by the user. To that end, we provide a method to compute the next entry's index, given the pair of ByteBuf slices returned by decodeMimeAndContentBuffers. Also renamed that method to decodeMimeAndContentBufferSlices, to make it more obvious that it produces slices (readerIndex impact and slices nature are mentioned in the javadoc). CompositeMetadata now keeps the nextEntryIndex as state and uses that for hasNext / decodeNext. Signed-off-by: Simon Baslé * CompositeMetadata as Iterable Previously, the implementation of CompositeMetadata resulted in a mutable type that didn't fit in with either imperative (for) or reactive (fromIterable) iteration strategies. This change updates the type to implement Iterable, and return an Iterator that lazily traverses the ByteBuf. There are also some improvements to the Flyweight in order to support this iteration style. Signed-off-by: Ben Hale * Apply googleFormat Signed-off-by: Simon Baslé --- .../metadata/WellKnownMimeTypePerf.java | 96 ++++ .../rsocket/metadata/CompositeMetadata.java | 220 ++++++++ .../metadata/CompositeMetadataFlyweight.java | 383 +++++++++++++ .../rsocket/metadata/WellKnownMimeType.java | 162 ++++++ .../java/io/rsocket/util/NumberUtils.java | 18 + .../CompositeMetadataFlyweightTest.java | 527 ++++++++++++++++++ .../metadata/CompositeMetadataTest.java | 157 ++++++ .../metadata/WellKnownMimeTypeTest.java | 74 +++ .../java/io/rsocket/util/NumberUtilsTest.java | 26 + 9 files changed, 1663 insertions(+) create mode 100644 rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java create mode 100644 rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java diff --git a/rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java b/rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java new file mode 100644 index 000000000..8f429fc19 --- /dev/null +++ b/rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java @@ -0,0 +1,96 @@ +package io.rsocket.metadata; + +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.Throughput) +@Fork(value = 1) +@Warmup(iterations = 10) +@Measurement(iterations = 10) +@State(Scope.Thread) +public class WellKnownMimeTypePerf { + + // this is the old values() looping implementation of fromIdentifier + private WellKnownMimeType fromIdValuesLoop(int id) { + if (id < 0 || id > 127) { + return WellKnownMimeType.UNPARSEABLE_MIME_TYPE; + } + for (WellKnownMimeType value : WellKnownMimeType.values()) { + if (value.getIdentifier() == id) { + return value; + } + } + return WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE; + } + + // this is the core of the old values() looping implementation of fromString + private WellKnownMimeType fromStringValuesLoop(String mimeType) { + for (WellKnownMimeType value : WellKnownMimeType.values()) { + if (mimeType.equals(value.getString())) { + return value; + } + } + return WellKnownMimeType.UNPARSEABLE_MIME_TYPE; + } + + @Benchmark + public void fromIdArrayLookup(final Blackhole bh) { + // negative lookup + bh.consume(WellKnownMimeType.fromIdentifier(-10)); + bh.consume(WellKnownMimeType.fromIdentifier(-1)); + // too large lookup + bh.consume(WellKnownMimeType.fromIdentifier(129)); + // first lookup + bh.consume(WellKnownMimeType.fromIdentifier(0)); + // middle lookup + bh.consume(WellKnownMimeType.fromIdentifier(37)); + // reserved lookup + bh.consume(WellKnownMimeType.fromIdentifier(63)); + // last lookup + bh.consume(WellKnownMimeType.fromIdentifier(127)); + } + + @Benchmark + public void fromIdValuesLoopLookup(final Blackhole bh) { + // negative lookup + bh.consume(fromIdValuesLoop(-10)); + bh.consume(fromIdValuesLoop(-1)); + // too large lookup + bh.consume(fromIdValuesLoop(129)); + // first lookup + bh.consume(fromIdValuesLoop(0)); + // middle lookup + bh.consume(fromIdValuesLoop(37)); + // reserved lookup + bh.consume(fromIdValuesLoop(63)); + // last lookup + bh.consume(fromIdValuesLoop(127)); + } + + @Benchmark + public void fromStringMapLookup(final Blackhole bh) { + // unknown lookup + bh.consume(WellKnownMimeType.fromString("foo/bar")); + // first lookup + bh.consume(WellKnownMimeType.fromString(WellKnownMimeType.APPLICATION_AVRO.getString())); + // middle lookup + bh.consume(WellKnownMimeType.fromString(WellKnownMimeType.VIDEO_VP8.getString())); + // last lookup + bh.consume( + WellKnownMimeType.fromString( + WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString())); + } + + @Benchmark + public void fromStringValuesLoopLookup(final Blackhole bh) { + // unknown lookup + bh.consume(fromStringValuesLoop("foo/bar")); + // first lookup + bh.consume(fromStringValuesLoop(WellKnownMimeType.APPLICATION_AVRO.getString())); + // middle lookup + bh.consume(fromStringValuesLoop(WellKnownMimeType.VIDEO_VP8.getString())); + // last lookup + bh.consume( + fromStringValuesLoop(WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString())); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java new file mode 100644 index 000000000..9eb349396 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java @@ -0,0 +1,220 @@ +/* + * Copyright 2015-2018 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.metadata; + +import static io.rsocket.metadata.CompositeMetadataFlyweight.computeNextEntryIndex; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeAndContentBuffersSlices; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeIdFromMimeBuffer; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer; +import static io.rsocket.metadata.CompositeMetadataFlyweight.hasEntry; +import static io.rsocket.metadata.CompositeMetadataFlyweight.isWellKnownMimeType; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.rsocket.metadata.CompositeMetadata.Entry; +import java.util.Iterator; +import reactor.util.annotation.Nullable; + +/** + * An {@link Iterable} wrapper around a {@link ByteBuf} that exposes metadata entry information at + * each decoding step. This is only possible on frame types used to initiate interactions, if the + * SETUP metadata mime type was {@link WellKnownMimeType#MESSAGE_RSOCKET_COMPOSITE_METADATA}. + * + *

This allows efficient incremental decoding of the entries (without moving the source's {@link + * io.netty.buffer.ByteBuf#readerIndex()}). The buffer is assumed to contain just enough bytes to + * represent one or more entries (mime type compressed or not). The decoding stops when the buffer + * reaches 0 readable bytes, and fails if it contains bytes but not enough to correctly decode an + * entry. + * + *

A note on future-proofness: it is possible to come across a compressed mime type that this + * implementation doesn't recognize. This is likely to be due to the use of a byte id that is merely + * reserved in this implementation, but maps to a {@link WellKnownMimeType} in the implementation + * that encoded the metadata. This can be detected by detecting that an entry is a {@link + * ReservedMimeTypeEntry}. In this case {@link Entry#getMimeType()} will return {@code null}. The + * encoded id can be retrieved using {@link ReservedMimeTypeEntry#getType()}. The byte and content + * buffer should be kept around and re-encoded using {@link + * CompositeMetadataFlyweight#encodeAndAddMetadata(CompositeByteBuf, ByteBufAllocator, byte, + * ByteBuf)} in case passing that entry through is required. + */ +public final class CompositeMetadata implements Iterable { + + private final boolean retainSlices; + + private final ByteBuf source; + + public CompositeMetadata(ByteBuf source, boolean retainSlices) { + this.source = source; + this.retainSlices = retainSlices; + } + + @Override + public Iterator iterator() { + return new Iterator() { + + private int entryIndex = 0; + + @Override + public boolean hasNext() { + return hasEntry(CompositeMetadata.this.source, this.entryIndex); + } + + @Override + public Entry next() { + ByteBuf[] headerAndData = + decodeMimeAndContentBuffersSlices( + CompositeMetadata.this.source, + this.entryIndex, + CompositeMetadata.this.retainSlices); + + ByteBuf header = headerAndData[0]; + ByteBuf data = headerAndData[1]; + + this.entryIndex = computeNextEntryIndex(this.entryIndex, header, data); + + if (!isWellKnownMimeType(header)) { + CharSequence typeString = decodeMimeTypeFromMimeBuffer(header); + if (typeString == null) { + throw new IllegalStateException("MIME type cannot be null"); + } + + return new ExplicitMimeTimeEntry(data, typeString.toString()); + } + + byte id = decodeMimeIdFromMimeBuffer(header); + WellKnownMimeType type = WellKnownMimeType.fromIdentifier(id); + + if (WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE == type) { + return new ReservedMimeTypeEntry(data, id); + } + + return new WellKnownMimeTypeEntry(data, type); + } + }; + } + + /** An entry in the {@link CompositeMetadata}. */ + public interface Entry { + + /** + * Returns the un-decoded content of the {@link Entry}. + * + * @return the un-decoded content of the {@link Entry} + */ + ByteBuf getContent(); + + /** + * Returns the MIME type of the entry, if it can be decoded. + * + * @return the MIME type of the entry, if it can be decoded, otherwise {@code null}. + */ + @Nullable + String getMimeType(); + } + + /** An {@link Entry} backed by an explicitly declared MIME type. */ + public static final class ExplicitMimeTimeEntry implements Entry { + + private final ByteBuf content; + + private final String type; + + public ExplicitMimeTimeEntry(ByteBuf content, String type) { + this.content = content; + this.type = type; + } + + @Override + public ByteBuf getContent() { + return this.content; + } + + @Override + public String getMimeType() { + return this.type; + } + } + + /** + * An {@link Entry} backed by a {@link WellKnownMimeType} entry, but one that is not understood by + * this implementation. + */ + public static final class ReservedMimeTypeEntry implements Entry { + private final ByteBuf content; + private final int type; + + public ReservedMimeTypeEntry(ByteBuf content, int type) { + this.content = content; + this.type = type; + } + + @Override + public ByteBuf getContent() { + return this.content; + } + + /** + * {@inheritDoc} Since this entry represents a compressed id that couldn't be decoded, this is + * always {@code null}. + */ + @Override + public String getMimeType() { + return null; + } + + /** + * Returns the reserved, but unknown {@link WellKnownMimeType} for this entry. Range is 0-127 + * (inclusive). + * + * @return the reserved, but unknown {@link WellKnownMimeType} for this entry + */ + public int getType() { + return this.type; + } + } + + /** An {@link Entry} backed by a {@link WellKnownMimeType}. */ + public static final class WellKnownMimeTypeEntry implements Entry { + + private final ByteBuf content; + private final WellKnownMimeType type; + + public WellKnownMimeTypeEntry(ByteBuf content, WellKnownMimeType type) { + this.content = content; + this.type = type; + } + + @Override + public ByteBuf getContent() { + return this.content; + } + + @Override + public String getMimeType() { + return this.type.getString(); + } + + /** + * Returns the {@link WellKnownMimeType} for this entry. + * + * @return the {@link WellKnownMimeType} for this entry + */ + public WellKnownMimeType getType() { + return this.type; + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java new file mode 100644 index 000000000..9abd638cb --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -0,0 +1,383 @@ +/* + * Copyright 2015-2018 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.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.netty.util.CharsetUtil; +import io.rsocket.util.NumberUtils; +import reactor.util.annotation.Nullable; + +/** + * A flyweight class that can be used to encode/decode composite metadata information to/from {@link + * ByteBuf}. This is intended for low-level efficient manipulation of such buffers. See {@link + * CompositeMetadata} for an Iterator-like approach to decoding entries. + */ +public class CompositeMetadataFlyweight { + + static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 + + static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 + + private CompositeMetadataFlyweight() {} + + public static int computeNextEntryIndex( + int currentEntryIndex, ByteBuf headerSlice, ByteBuf contentSlice) { + return currentEntryIndex + + headerSlice.readableBytes() // this includes the mime length byte + + 3 // 3 bytes of the content length, which are excluded from the slice + + contentSlice.readableBytes(); + } + + /** + * Decode the next metadata entry (a mime header + content pair of {@link ByteBuf}) from a {@link + * ByteBuf} that contains at least enough bytes for one more such entry. These buffers are + * actually slices of the full metadata buffer, and this method doesn't move the full metadata + * buffer's {@link ByteBuf#readerIndex()}. As such, it requires the user to provide an {@code + * index} to read from. The next index is computed by calling {@link #computeNextEntryIndex(int, + * ByteBuf, ByteBuf)}. Size of the first buffer (the "header buffer") drives which decoding method + * should be further applied to it. + * + *

The header buffer is either: + * + *

    + *
  • made up of a single byte: this represents an encoded mime id, which can be further + * decoded using {@link #decodeMimeIdFromMimeBuffer(ByteBuf)} + *
  • made up of 2 or more bytes: this represents an encoded mime String + its length, which + * can be further decoded using {@link #decodeMimeTypeFromMimeBuffer(ByteBuf)}. Note the + * encoded length, in the first byte, is skipped by this decoding method because the + * remaining length of the buffer is that of the mime string. + *
+ * + * @param compositeMetadata the source {@link ByteBuf} that originally contains one or more + * metadata entries + * @param entryIndex the {@link ByteBuf#readerIndex()} to start decoding from. original reader + * index is kept on the source buffer + * @param retainSlices should produced metadata entry buffers {@link ByteBuf#slice() slices} be + * {@link ByteBuf#retainedSlice() retained}? + * @return a {@link ByteBuf} array of length 2 containing the mime header buffer + * slice and the content buffer slice, or one of the + * zero-length error constant arrays + */ + public static ByteBuf[] decodeMimeAndContentBuffersSlices( + ByteBuf compositeMetadata, int entryIndex, boolean retainSlices) { + compositeMetadata.markReaderIndex(); + compositeMetadata.readerIndex(entryIndex); + + if (compositeMetadata.isReadable()) { + ByteBuf mime; + int ridx = compositeMetadata.readerIndex(); + byte mimeIdOrLength = compositeMetadata.readByte(); + if ((mimeIdOrLength & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK) { + mime = + retainSlices + ? compositeMetadata.retainedSlice(ridx, 1) + : compositeMetadata.slice(ridx, 1); + } else { + // M flag unset, remaining 7 bits are the length of the mime + int mimeLength = Byte.toUnsignedInt(mimeIdOrLength) + 1; + + if (compositeMetadata.isReadable( + mimeLength)) { // need to be able to read an extra mimeLength bytes + // here we need a way for the returned ByteBuf to differentiate between a + // 1-byte length mime type and a 1 byte encoded mime id, preferably without + // re-applying the byte mask. The easiest way is to include the initial byte + // and have further decoding ignore the first byte. 1 byte buffer == id, 2+ byte + // buffer == full mime string. + mime = + retainSlices + ? + // we accommodate that we don't read from current readerIndex, but + // readerIndex - 1 ("0"), for a total slice size of mimeLength + 1 + compositeMetadata.retainedSlice(ridx, mimeLength + 1) + : compositeMetadata.slice(ridx, mimeLength + 1); + // we thus need to skip the bytes we just sliced, but not the flag/length byte + // which was already skipped in initial read + compositeMetadata.skipBytes(mimeLength); + } else { + compositeMetadata.resetReaderIndex(); + throw new IllegalStateException("metadata is malformed"); + } + } + + if (compositeMetadata.isReadable(3)) { + // ensures the length medium can be read + final int metadataLength = compositeMetadata.readUnsignedMedium(); + if (compositeMetadata.isReadable(metadataLength)) { + ByteBuf metadata = + retainSlices + ? compositeMetadata.readRetainedSlice(metadataLength) + : compositeMetadata.readSlice(metadataLength); + compositeMetadata.resetReaderIndex(); + return new ByteBuf[] {mime, metadata}; + } else { + compositeMetadata.resetReaderIndex(); + throw new IllegalStateException("metadata is malformed"); + } + } else { + compositeMetadata.resetReaderIndex(); + throw new IllegalStateException("metadata is malformed"); + } + } + compositeMetadata.resetReaderIndex(); + throw new IllegalArgumentException( + String.format("entry index %d is larger than buffer size", entryIndex)); + } + + /** + * Decode a {@code byte} compressed mime id from a {@link ByteBuf}, assuming said buffer properly + * contains such an id. + * + *

The buffer must have exactly one readable byte, which is assumed to have been tested for + * mime id encoding via the {@link #STREAM_METADATA_KNOWN_MASK} mask ({@code firstByte & + * STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK}). + * + *

If there is no readable byte, the negative identifier of {@link + * WellKnownMimeType#UNPARSEABLE_MIME_TYPE} is returned. + * + * @param mimeBuffer the buffer that should next contain the compressed mime id byte + * @return the compressed mime id, between 0 and 127, or a negative id if the input is invalid + * @see #decodeMimeTypeFromMimeBuffer(ByteBuf) + */ + public static byte decodeMimeIdFromMimeBuffer(ByteBuf mimeBuffer) { + if (mimeBuffer.readableBytes() != 1) { + return WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier(); + } + return (byte) (mimeBuffer.readByte() & STREAM_METADATA_LENGTH_MASK); + } + + /** + * Decode a {@link CharSequence} custome mime type from a {@link ByteBuf}, assuming said buffer + * properly contains such a mime type. + * + *

The buffer must at least have two readable bytes, which distinguishes it from the {@link + * #decodeMimeIdFromMimeBuffer(ByteBuf) compressed id} case. The first byte is a size and the + * remaining bytes must correspond to the {@link CharSequence}, encoded fully in US_ASCII. As a + * result, the first byte can simply be skipped, and the remaining of the buffer be decoded to the + * mime type. + * + *

If the mime header buffer is less than 2 bytes long, returns {@code null}. + * + * @param flyweightMimeBuffer the mime header {@link ByteBuf} that contains length + custom mime + * type + * @return the decoded custom mime type, as a {@link CharSequence}, or null if the input is + * invalid + * @see #decodeMimeIdFromMimeBuffer(ByteBuf) + */ + @Nullable + public static CharSequence decodeMimeTypeFromMimeBuffer(ByteBuf flyweightMimeBuffer) { + if (flyweightMimeBuffer.readableBytes() < 2) { + throw new IllegalStateException("unable to decode explicit MIME type"); + } + // the encoded length is assumed to be kept at the start of the buffer + // but also assumed to be irrelevant because the rest of the slice length + // actually already matches _decoded_length + flyweightMimeBuffer.skipBytes(1); + int mimeStringLength = flyweightMimeBuffer.readableBytes(); + return flyweightMimeBuffer.readCharSequence(mimeStringLength, CharsetUtil.US_ASCII); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}, without checking if the {@link String} can be matched with a well known compressable + * mime type. Prefer using this method and {@link #encodeAndAddMetadata(CompositeByteBuf, + * ByteBufAllocator, WellKnownMimeType, ByteBuf)} if you know in advance whether or not the mime + * is well known. Otherwise use {@link #encodeAndAddMetadataWithCompression(CompositeByteBuf, + * ByteBufAllocator, String, ByteBuf)} + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param customMimeType the custom mime type to encode. + * @param metadata the metadata value to encode. + */ + // see #encodeMetadataHeader(ByteBufAllocator, String, int) + public static void encodeAndAddMetadata( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + String customMimeType, + ByteBuf metadata) { + compositeMetaData.addComponents( + true, encodeMetadataHeader(allocator, customMimeType, metadata.readableBytes()), metadata); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}. + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param knownMimeType the {@link WellKnownMimeType} to encode. + * @param metadata the metadata value to encode. + */ + // see #encodeMetadataHeader(ByteBufAllocator, byte, int) + public static void encodeAndAddMetadata( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + WellKnownMimeType knownMimeType, + ByteBuf metadata) { + compositeMetaData.addComponents( + true, + encodeMetadataHeader(allocator, knownMimeType.getIdentifier(), metadata.readableBytes()), + metadata); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}, first verifying if the passed {@link String} matches a {@link WellKnownMimeType} (in + * which case it will be encoded in a compressed fashion using the mime id of that type). + * + *

Prefer using {@link #encodeAndAddMetadata(CompositeByteBuf, ByteBufAllocator, String, + * ByteBuf)} if you already know that the mime type is not a {@link WellKnownMimeType}. + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param mimeType the mime type to encode, as a {@link String}. well known mime types are + * compressed. + * @param metadata the metadata value to encode. + * @see #encodeAndAddMetadata(CompositeByteBuf, ByteBufAllocator, WellKnownMimeType, ByteBuf) + */ + // see #encodeMetadataHeader(ByteBufAllocator, String, int) + public static void encodeAndAddMetadataWithCompression( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + String mimeType, + ByteBuf metadata) { + WellKnownMimeType wkn = WellKnownMimeType.fromString(mimeType); + if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { + compositeMetaData.addComponents( + true, encodeMetadataHeader(allocator, mimeType, metadata.readableBytes()), metadata); + } else { + compositeMetaData.addComponents( + true, + encodeMetadataHeader(allocator, wkn.getIdentifier(), metadata.readableBytes()), + metadata); + } + } + + /** + * Returns whether there is another entry available at a given index + * + * @param compositeMetadata the buffer to inspect + * @param entryIndex the index to check at + * @return whether there is another entry available at a given index + */ + public static boolean hasEntry(ByteBuf compositeMetadata, int entryIndex) { + return compositeMetadata.writerIndex() - entryIndex > 0; + } + + /** + * Returns whether the header represents a well-known MIME type. + * + * @param header the header to inspect + * @return whether the header represents a well-known MIME type + */ + public static boolean isWellKnownMimeType(ByteBuf header) { + return header.readableBytes() == 1; + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}. + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param unknownCompressedMimeType the id of the {@link + * WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE} to encode. + * @param metadata the metadata value to encode. + */ + // see #encodeMetadataHeader(ByteBufAllocator, byte, int) + static void encodeAndAddMetadata( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + byte unknownCompressedMimeType, + ByteBuf metadata) { + compositeMetaData.addComponents( + true, + encodeMetadataHeader(allocator, unknownCompressedMimeType, metadata.readableBytes()), + metadata); + } + + /** + * Encode a custom mime type and a metadata value length into a newly allocated {@link ByteBuf}. + * + *

This larger representation encodes the mime type representation's length on a single byte, + * then the representation itself, then the unsigned metadata value length on 3 additional bytes. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer. + * @param customMime a custom mime type to encode. + * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits + * integer. + * @return the encoded mime and metadata length information + */ + static ByteBuf encodeMetadataHeader( + ByteBufAllocator allocator, String customMime, int metadataLength) { + ByteBuf metadataHeader = allocator.buffer(4 + customMime.length()); + // reserve 1 byte for the customMime length + int writerIndexInitial = metadataHeader.writerIndex(); + metadataHeader.writerIndex(writerIndexInitial + 1); + + // write the custom mime in UTF8 but validate it is all ASCII-compatible + // (which produces the right result since ASCII chars are still encoded on 1 byte in UTF8) + int customMimeLength = ByteBufUtil.writeUtf8(metadataHeader, customMime); + if (!ByteBufUtil.isText(metadataHeader, CharsetUtil.US_ASCII)) { + metadataHeader.release(); + throw new IllegalArgumentException("custom mime type must be US_ASCII characters only"); + } + if (customMimeLength < 1 || customMimeLength > 128) { + metadataHeader.release(); + throw new IllegalArgumentException( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + metadataHeader.markWriterIndex(); + + // go back to beginning and write the length + // encoded length is one less than actual length, since 0 is never a valid length, which gives + // wider representation range + metadataHeader.writerIndex(writerIndexInitial); + metadataHeader.writeByte(customMimeLength - 1); + + // go back to post-mime type and write the metadata content length + metadataHeader.resetWriterIndex(); + NumberUtils.encodeUnsignedMedium(metadataHeader, metadataLength); + + return metadataHeader; + } + + /** + * Encode a {@link WellKnownMimeType well known mime type} and a metadata value length into a + * newly allocated {@link ByteBuf}. + * + *

This compact representation encodes the mime type via its ID on a single byte, and the + * unsigned value length on 3 additional bytes. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer. + * @param mimeType a byte identifier of a {@link WellKnownMimeType} to encode. + * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits + * integer. + * @return the encoded mime and metadata length information + */ + static ByteBuf encodeMetadataHeader( + ByteBufAllocator allocator, byte mimeType, int metadataLength) { + ByteBuf buffer = allocator.buffer(4, 4).writeByte(mimeType | STREAM_METADATA_KNOWN_MASK); + + NumberUtils.encodeUnsignedMedium(buffer, metadataLength); + + return buffer; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java new file mode 100644 index 000000000..9ecaf0859 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java @@ -0,0 +1,162 @@ +/* + * Copyright 2015-2018 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.metadata; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Enumeration of Well Known Mime Types, as defined in the eponymous extension. Such mime types are + * used in composite metadata (which can include routing and/or tracing metadata). Per + * specification, identifiers are between 0 and 127 (inclusive). + */ +public enum WellKnownMimeType { + UNPARSEABLE_MIME_TYPE("UNPARSEABLE_MIME_TYPE_DO_NOT_USE", (byte) -2), + UNKNOWN_RESERVED_MIME_TYPE("UNKNOWN_YET_RESERVED_DO_NOT_USE", (byte) -1), + + APPLICATION_AVRO("application/avro", (byte) 0x00), + APPLICATION_CBOR("application/cbor", (byte) 0x01), + APPLICATION_GRAPHQL("application/graphql", (byte) 0x02), + APPLICATION_GZIP("application/gzip", (byte) 0x03), + APPLICATION_JAVASCRIPT("application/javascript", (byte) 0x04), + APPLICATION_JSON("application/json", (byte) 0x05), + APPLICATION_OCTET_STREAM("application/octet-stream", (byte) 0x06), + APPLICATION_PDF("application/pdf", (byte) 0x07), + APPLICATION_THRIFT("application/vnd.apache.thrift.binary", (byte) 0x08), + APPLICATION_PROTOBUF("application/vnd.google.protobuf", (byte) 0x09), + APPLICATION_XML("application/xml", (byte) 0x0A), + APPLICATION_ZIP("application/zip", (byte) 0x0B), + AUDIO_AAC("audio/aac", (byte) 0x0C), + AUDIO_MP3("audio/mp3", (byte) 0x0D), + AUDIO_MP4("audio/mp4", (byte) 0x0E), + AUDIO_MPEG3("audio/mpeg3", (byte) 0x0F), + AUDIO_MPEG("audio/mpeg", (byte) 0x10), + AUDIO_OGG("audio/ogg", (byte) 0x11), + AUDIO_OPUS("audio/opus", (byte) 0x12), + AUDIO_VORBIS("audio/vorbis", (byte) 0x13), + IMAGE_BMP("image/bmp", (byte) 0x14), + IMAGE_GIG("image/gif", (byte) 0x15), + IMAGE_HEIC_SEQUENCE("image/heic-sequence", (byte) 0x16), + IMAGE_HEIC("image/heic", (byte) 0x17), + IMAGE_HEIF_SEQUENCE("image/heif-sequence", (byte) 0x18), + IMAGE_HEIF("image/heif", (byte) 0x19), + IMAGE_JPEG("image/jpeg", (byte) 0x1A), + IMAGE_PNG("image/png", (byte) 0x1B), + IMAGE_TIFF("image/tiff", (byte) 0x1C), + MULTIPART_MIXED("multipart/mixed", (byte) 0x1D), + TEXT_CSS("text/css", (byte) 0x1E), + TEXT_CSV("text/csv", (byte) 0x1F), + TEXT_HTML("text/html", (byte) 0x20), + TEXT_PLAIN("text/plain", (byte) 0x21), + TEXT_XML("text/xml", (byte) 0x22), + VIDEO_H264("video/H264", (byte) 0x23), + VIDEO_H265("video/H265", (byte) 0x24), + VIDEO_VP8("video/VP8", (byte) 0x25), + + // ... reserved for future use ... + + MESSAGE_RSOCKET_TRACING_ZIPKIN("message/x.rsocket.tracing-zipkin.v0", (byte) 0x7D), + MESSAGE_RSOCKET_ROUTING("message/x.rsocket.routing.v0", (byte) 0x7E), + MESSAGE_RSOCKET_COMPOSITE_METADATA("message/x.rsocket.composite-metadata.v0", (byte) 0x7F); + + static final WellKnownMimeType[] TYPES_BY_MIME_ID; + static final Map TYPES_BY_MIME_STRING; + + static { + // precompute an array of all valid mime ids, filling the blanks with the RESERVED enum + TYPES_BY_MIME_ID = new WellKnownMimeType[128]; // 0-127 inclusive + Arrays.fill(TYPES_BY_MIME_ID, UNKNOWN_RESERVED_MIME_TYPE); + // also prepare a Map of the types by mime string + TYPES_BY_MIME_STRING = new HashMap<>(128); + + for (WellKnownMimeType value : values()) { + if (value.getIdentifier() >= 0) { + TYPES_BY_MIME_ID[value.getIdentifier()] = value; + TYPES_BY_MIME_STRING.put(value.getString(), value); + } + } + } + + private final byte identifier; + private final String str; + + WellKnownMimeType(String str, byte identifier) { + this.str = str; + this.identifier = identifier; + } + + /** + * Find the {@link WellKnownMimeType} for the given identifier (as an {@code int}). Valid + * identifiers are defined to be integers between 0 and 127, inclusive. Identifiers outside of + * this range will produce the {@link #UNPARSEABLE_MIME_TYPE}. Additionally, some identifiers in + * that range are still only reserved and don't have a type associated yet: this method returns + * the {@link #UNKNOWN_RESERVED_MIME_TYPE} when passing such an identifier, which lets call sites + * potentially detect this and keep the original representation when transmitting the associated + * metadata buffer. + * + * @param id the looked up identifier + * @return the {@link WellKnownMimeType}, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is out + * of the specification's range, or {@link #UNKNOWN_RESERVED_MIME_TYPE} if the id is one that + * is merely reserved but unknown to this implementation. + */ + public static WellKnownMimeType fromIdentifier(int id) { + if (id < 0x00 || id > 0x7F) { + return UNPARSEABLE_MIME_TYPE; + } + return TYPES_BY_MIME_ID[id]; + } + + /** + * Find the {@link WellKnownMimeType} for the given {@link String} representation. If the + * representation is {@code null} or doesn't match a {@link WellKnownMimeType}, the {@link + * #UNPARSEABLE_MIME_TYPE} is returned. + * + * @param mimeType the looked up mime type + * @return the matching {@link WellKnownMimeType}, or {@link #UNPARSEABLE_MIME_TYPE} if none + * matches + */ + public static WellKnownMimeType fromString(String mimeType) { + if (mimeType == null) throw new IllegalArgumentException("type must be non-null"); + + // force UNPARSEABLE if by chance UNKNOWN_RESERVED_MIME_TYPE's text has been used + if (mimeType.equals(UNKNOWN_RESERVED_MIME_TYPE.str)) { + return UNPARSEABLE_MIME_TYPE; + } + + return TYPES_BY_MIME_STRING.getOrDefault(mimeType, UNPARSEABLE_MIME_TYPE); + } + + /** @return the byte identifier of the mime type, guaranteed to be positive or zero. */ + public byte getIdentifier() { + return identifier; + } + + /** + * @return the mime type represented as a {@link String}, which is made of US_ASCII compatible + * characters only + */ + public String getString() { + return str; + } + + /** @see #getString() */ + @Override + public String toString() { + return str; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java b/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java index 12e3cee45..3ff720447 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java +++ b/rsocket-core/src/main/java/io/rsocket/util/NumberUtils.java @@ -16,6 +16,7 @@ package io.rsocket.util; +import io.netty.buffer.ByteBuf; import java.util.Objects; public final class NumberUtils { @@ -143,4 +144,21 @@ public static int requireUnsignedShort(int i) { return i; } + + /** + * Encode an unsigned medium integer on 3 bytes / 24 bits. This can be decoded directly by the + * {@link ByteBuf#readUnsignedMedium()} method. + * + * @param byteBuf the {@link ByteBuf} into which to write the bits + * @param i the medium integer to encode + * @see #requireUnsignedMedium(int) + */ + public static void encodeUnsignedMedium(ByteBuf byteBuf, int i) { + requireUnsignedMedium(i); + // Write each byte separately in reverse order, this mean we can write 1 << 23 without + // overflowing. + byteBuf.writeByte(i >> 16); + byteBuf.writeByte(i >> 8); + byteBuf.writeByte(i); + } } diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java new file mode 100644 index 000000000..1a22e9e23 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java @@ -0,0 +1,527 @@ +/* + * Copyright 2015-2018 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.metadata; + +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeAndContentBuffersSlices; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeIdFromMimeBuffer; +import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.netty.util.CharsetUtil; +import io.rsocket.test.util.ByteBufUtils; +import io.rsocket.util.NumberUtils; +import org.junit.jupiter.api.Test; + +class CompositeMetadataFlyweightTest { + + static String byteToBitsString(byte b) { + return String.format("%8s", Integer.toBinaryString(b & 0xFF)).replace(' ', '0'); + } + + static String toHeaderBits(ByteBuf encoded) { + encoded.markReaderIndex(); + byte headerByte = encoded.readByte(); + String byteAsString = byteToBitsString(headerByte); + encoded.resetReaderIndex(); + return byteAsString; + } + // ==== + + @Test + void customMimeHeaderLatin1_encodingFails() { + String mimeNotAscii = "mime/typé"; + + assertThatIllegalArgumentException() + .isThrownBy( + () -> + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + .withMessage("custom mime type must be US_ASCII characters only"); + } + + @Test + void customMimeHeaderLength0_encodingFails() { + assertThatIllegalArgumentException() + .isThrownBy( + () -> CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "", 0)) + .withMessage( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + @Test + void customMimeHeaderLength127() { + StringBuilder builder = new StringBuilder(127); + for (int i = 0; i < 127; i++) { + builder.append('a'); + } + String mimeString = builder.toString(); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + + // remember actual length = encoded length + 1 + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111110"); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); + + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(127 - 1); // encoded as actual length - 1 + + assertThat(header.readCharSequence(127, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); + + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void customMimeHeaderLength128() { + StringBuilder builder = new StringBuilder(128); + for (int i = 0; i < 128; i++) { + builder.append('a'); + } + String mimeString = builder.toString(); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + + // remember actual length = encoded length + 1 + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("01111111"); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); + + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(128 - 1); // encoded as actual length - 1 + + assertThat(header.readCharSequence(128, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); + + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void customMimeHeaderLength129_encodingFails() { + StringBuilder builder = new StringBuilder(129); + for (int i = 0; i < 129; i++) { + builder.append('a'); + } + + assertThatIllegalArgumentException() + .isThrownBy( + () -> + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, builder.toString(), 0)) + .withMessage( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + @Test + void customMimeHeaderLengthOne() { + String mimeString = "w"; + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + + // remember actual length = encoded length + 1 + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000000"); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); + + assertThat((int) header.readByte()).as("mime length").isZero(); // encoded as actual length - 1 + + assertThat(header.readCharSequence(1, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); + + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void customMimeHeaderLengthTwo() { + String mimeString = "ww"; + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mimeString, 0); + + // remember actual length = encoded length + 1 + assertThat(toHeaderBits(encoded)).startsWith("0").isEqualTo("00000001"); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isGreaterThan(1); + + assertThat((int) header.readByte()) + .as("mime length") + .isEqualTo(2 - 1); // encoded as actual length - 1 + + assertThat(header.readCharSequence(2, CharsetUtil.US_ASCII)) + .as("mime string") + .hasToString(mimeString); + + header.resetReaderIndex(); + assertThat(CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer(header)) + .as("decoded mime string") + .hasToString(mimeString); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void customMimeHeaderUtf8_encodingFails() { + String mimeNotAscii = + "mime/tyࠒe"; // this is the SAMARITAN LETTER QUF u+0812 represented on 3 bytes + assertThatIllegalArgumentException() + .isThrownBy( + () -> + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mimeNotAscii, 0)) + .withMessage("custom mime type must be US_ASCII characters only"); + } + + @Test + void decodeEntryAtEndOfBuffer() { + ByteBuf fakeEntry = Unpooled.buffer(); + + assertThatIllegalArgumentException() + .isThrownBy(() -> decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)); + } + + @Test + void decodeEntryHasNoContentLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(0); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)); + } + + @Test + void decodeEntryTooShortForContentLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(1); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + NumberUtils.encodeUnsignedMedium(fakeEntry, 456); + fakeEntry.writeChar('w'); + + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)); + } + + @Test + void decodeEntryTooShortForMimeLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(120); + + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeAndContentBuffersSlices(fakeEntry, 0, false)); + } + + @Test + void decodeIdMinusTwoWhenMoreThanOneByte() { + ByteBuf fakeIdBuffer = Unpooled.buffer(2); + fakeIdBuffer.writeInt(200); + + assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) + .isEqualTo((WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier())); + } + + @Test + void decodeIdMinusTwoWhenZeroByte() { + ByteBuf fakeIdBuffer = Unpooled.buffer(0); + + assertThat(decodeMimeIdFromMimeBuffer(fakeIdBuffer)) + .isEqualTo((WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier())); + } + + @Test + void decodeStringNullIfLengthOne() { + ByteBuf fakeTypeBuffer = Unpooled.buffer(2); + fakeTypeBuffer.writeByte(1); + + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)); + } + + @Test + void decodeStringNullIfLengthZero() { + ByteBuf fakeTypeBuffer = Unpooled.buffer(2); + + assertThatIllegalStateException() + .isThrownBy(() -> decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)); + } + + @Test + void decodeTypeSkipsFirstByte() { + ByteBuf fakeTypeBuffer = Unpooled.buffer(2); + fakeTypeBuffer.writeByte(128); + fakeTypeBuffer.writeCharSequence("example", CharsetUtil.US_ASCII); + + assertThat(decodeMimeTypeFromMimeBuffer(fakeTypeBuffer)).hasToString("example"); + } + + @Test + void encodeMetadataCustomTypeDelegates() { + ByteBuf expected = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, "foo", 2); + + CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadata( + test, ByteBufAllocator.DEFAULT, "foo", ByteBufUtils.getRandomByteBuf(2)); + + assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + } + + @Test + void encodeMetadataKnownTypeDelegates() { + ByteBuf expected = + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, + WellKnownMimeType.APPLICATION_OCTET_STREAM.getIdentifier(), + 2); + + CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadata( + test, + ByteBufAllocator.DEFAULT, + WellKnownMimeType.APPLICATION_OCTET_STREAM, + ByteBufUtils.getRandomByteBuf(2)); + + assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + } + + @Test + void encodeMetadataReservedTypeDelegates() { + ByteBuf expected = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, (byte) 120, 2); + + CompositeByteBuf test = ByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadata( + test, ByteBufAllocator.DEFAULT, (byte) 120, ByteBufUtils.getRandomByteBuf(2)); + + assertThat((Iterable) test).hasSize(2).first().isEqualTo(expected); + } + + @Test + void encodeTryCompressWithCompressableType() { + ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); + CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadataWithCompression( + target, + UnpooledByteBufAllocator.DEFAULT, + WellKnownMimeType.APPLICATION_AVRO.getString(), + metadata); + + assertThat(target.readableBytes()).as("readableBytes 1 + 3 + 2").isEqualTo(6); + } + + @Test + void encodeTryCompressWithCustomType() { + ByteBuf metadata = ByteBufUtils.getRandomByteBuf(2); + CompositeByteBuf target = UnpooledByteBufAllocator.DEFAULT.compositeBuffer(); + + CompositeMetadataFlyweight.encodeAndAddMetadataWithCompression( + target, UnpooledByteBufAllocator.DEFAULT, "custom/example", metadata); + + assertThat(target.readableBytes()).as("readableBytes 1 + 14 + 3 + 2").isEqualTo(20); + } + + @Test + void hasEntry() { + WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; + + CompositeByteBuf buffer = + Unpooled.compositeBuffer() + .addComponent( + true, + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0)) + .addComponent( + true, + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0)); + + assertThat(CompositeMetadataFlyweight.hasEntry(buffer, 0)).isTrue(); + assertThat(CompositeMetadataFlyweight.hasEntry(buffer, 4)).isTrue(); + assertThat(CompositeMetadataFlyweight.hasEntry(buffer, 8)).isFalse(); + } + + @Test + void isWellKnownMimeType() { + ByteBuf wellKnown = Unpooled.buffer().writeByte(0); + assertThat(CompositeMetadataFlyweight.isWellKnownMimeType(wellKnown)).isTrue(); + + ByteBuf explicit = Unpooled.buffer().writeByte(2).writeChar('a'); + assertThat(CompositeMetadataFlyweight.isWellKnownMimeType(explicit)).isFalse(); + } + + @Test + void knownMimeHeader120_reserved() { + byte mime = (byte) 120; + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader(ByteBufAllocator.DEFAULT, mime, 0); + + assertThat(mime) + .as("smoke test RESERVED_120 unsigned 7 bits representation") + .isEqualTo((byte) 0b01111000); + + assertThat(toHeaderBits(encoded)).startsWith("1").isEqualTo("11111000"); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isOne(); + + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("11111000"); + + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)).as("decoded mime id").isEqualTo(mime); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void knownMimeHeader127_compositeMetadata() { + WellKnownMimeType mime = WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA; + assertThat(mime.getIdentifier()) + .as("smoke test COMPOSITE unsigned 7 bits representation") + .isEqualTo((byte) 127) + .isEqualTo((byte) 0b01111111); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("1") + .isEqualTo("11111111") + .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isOne(); + + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("11111111"); + + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)) + .as("decoded mime id") + .isEqualTo(mime.getIdentifier()); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } + + @Test + void knownMimeHeaderZero_avro() { + WellKnownMimeType mime = WellKnownMimeType.APPLICATION_AVRO; + assertThat(mime.getIdentifier()) + .as("smoke test AVRO unsigned 7 bits representation") + .isEqualTo((byte) 0) + .isEqualTo((byte) 0b00000000); + ByteBuf encoded = + CompositeMetadataFlyweight.encodeMetadataHeader( + ByteBufAllocator.DEFAULT, mime.getIdentifier(), 0); + + assertThat(toHeaderBits(encoded)) + .startsWith("1") + .isEqualTo("10000000") + .isEqualTo(byteToBitsString(mime.getIdentifier()).replaceFirst("0", "1")); + + final ByteBuf[] byteBufs = decodeMimeAndContentBuffersSlices(encoded, 0, false); + assertThat(byteBufs).hasSize(2).doesNotContainNull(); + + ByteBuf header = byteBufs[0]; + ByteBuf content = byteBufs[1]; + header.markReaderIndex(); + + assertThat(header.readableBytes()).as("metadata header size").isOne(); + + assertThat(byteToBitsString(header.readByte())) + .as("header bit representation") + .isEqualTo("10000000"); + + header.resetReaderIndex(); + assertThat(decodeMimeIdFromMimeBuffer(header)) + .as("decoded mime id") + .isEqualTo(mime.getIdentifier()); + + assertThat(content.readableBytes()).as("no metadata content").isZero(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java new file mode 100644 index 000000000..cc00df7d4 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java @@ -0,0 +1,157 @@ +/* + * Copyright 2015-2018 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.metadata; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.rsocket.metadata.CompositeMetadata.Entry; +import io.rsocket.metadata.CompositeMetadata.ReservedMimeTypeEntry; +import io.rsocket.metadata.CompositeMetadata.WellKnownMimeTypeEntry; +import io.rsocket.test.util.ByteBufUtils; +import io.rsocket.util.NumberUtils; +import java.util.Iterator; +import org.junit.jupiter.api.Test; + +class CompositeMetadataTest { + + @Test + void decodeEntryHasNoContentLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(0); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeEntry, false); + + assertThatIllegalStateException() + .isThrownBy(() -> compositeMetadata.iterator().next()) + .withMessage("metadata is malformed"); + } + + @Test + void decodeEntryOnDoneBufferThrowsIllegalArgument() { + ByteBuf fakeBuffer = ByteBufUtils.getRandomByteBuf(0); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeBuffer, false); + + assertThatIllegalArgumentException() + .isThrownBy(() -> compositeMetadata.iterator().next()) + .withMessage("entry index 0 is larger than buffer size"); + } + + @Test + void decodeEntryTooShortForContentLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(1); + fakeEntry.writeCharSequence("w", CharsetUtil.US_ASCII); + NumberUtils.encodeUnsignedMedium(fakeEntry, 456); + fakeEntry.writeChar('w'); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeEntry, false); + + assertThatIllegalStateException() + .isThrownBy(() -> compositeMetadata.iterator().next()) + .withMessage("metadata is malformed"); + } + + @Test + void decodeEntryTooShortForMimeLength() { + ByteBuf fakeEntry = Unpooled.buffer(); + fakeEntry.writeByte(120); + CompositeMetadata compositeMetadata = new CompositeMetadata(fakeEntry, false); + + assertThatIllegalStateException() + .isThrownBy(() -> compositeMetadata.iterator().next()) + .withMessage("metadata is malformed"); + } + + @Test + void decodeThreeEntries() { + // metadata 1: well known + WellKnownMimeType mimeType1 = WellKnownMimeType.APPLICATION_PDF; + ByteBuf metadata1 = Unpooled.buffer(); + metadata1.writeCharSequence("abcdefghijkl", CharsetUtil.UTF_8); + + // metadata 2: custom + String mimeType2 = "application/custom"; + ByteBuf metadata2 = Unpooled.buffer(); + metadata2.writeChar('E'); + metadata2.writeChar('∑'); + metadata2.writeChar('é'); + metadata2.writeBoolean(true); + metadata2.writeChar('W'); + + // metadata 3: reserved but unknown + byte reserved = 120; + assertThat(WellKnownMimeType.fromIdentifier(reserved)) + .as("ensure UNKNOWN RESERVED used in test") + .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); + ByteBuf metadata3 = Unpooled.buffer(); + metadata3.writeByte(88); + + CompositeByteBuf compositeMetadataBuffer = ByteBufAllocator.DEFAULT.compositeBuffer(); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadataBuffer, ByteBufAllocator.DEFAULT, mimeType1, metadata1); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadataBuffer, ByteBufAllocator.DEFAULT, mimeType2, metadata2); + CompositeMetadataFlyweight.encodeAndAddMetadata( + compositeMetadataBuffer, ByteBufAllocator.DEFAULT, reserved, metadata3); + + Iterator iterator = new CompositeMetadata(compositeMetadataBuffer, true).iterator(); + + assertThat(iterator.next()) + .as("entry1") + .isNotNull() + .satisfies( + e -> + assertThat(e.getMimeType()).as("entry1 mime type").isEqualTo(mimeType1.getString())) + .satisfies( + e -> + assertThat(((WellKnownMimeTypeEntry) e).getType()) + .as("entry1 mime id") + .isEqualTo(WellKnownMimeType.APPLICATION_PDF)) + .satisfies( + e -> + assertThat(e.getContent().toString(CharsetUtil.UTF_8)) + .as("entry1 decoded") + .isEqualTo("abcdefghijkl")); + + assertThat(iterator.next()) + .as("entry2") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()).as("entry2 mime type").isEqualTo(mimeType2)) + .satisfies( + e -> assertThat(e.getContent()).as("entry2 decoded").isEqualByComparingTo(metadata2)); + + assertThat(iterator.next()) + .as("entry3") + .isNotNull() + .satisfies(e -> assertThat(e.getMimeType()).as("entry3 mime type").isNull()) + .satisfies( + e -> + assertThat(((ReservedMimeTypeEntry) e).getType()) + .as("entry3 mime id") + .isEqualTo(reserved)) + .satisfies( + e -> assertThat(e.getContent()).as("entry3 decoded").isEqualByComparingTo(metadata3)); + + assertThat(iterator.hasNext()).as("has no more than 3 entries").isFalse(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java new file mode 100644 index 000000000..316aaf091 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/WellKnownMimeTypeTest.java @@ -0,0 +1,74 @@ +/* + * Copyright 2015-2018 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.metadata; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class WellKnownMimeTypeTest { + + @Test + void fromIdentifierGreaterThan127() { + assertThat(WellKnownMimeType.fromIdentifier(128)) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } + + @Test + void fromIdentifierMatchFromMimeType() { + for (WellKnownMimeType mimeType : WellKnownMimeType.values()) { + if (mimeType == WellKnownMimeType.UNPARSEABLE_MIME_TYPE + || mimeType == WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE) { + continue; + } + assertThat(WellKnownMimeType.fromString(mimeType.toString())) + .as("mimeType string for " + mimeType.name()) + .isSameAs(mimeType); + + assertThat(WellKnownMimeType.fromIdentifier(mimeType.getIdentifier())) + .as("mimeType ID for " + mimeType.name()) + .isSameAs(mimeType); + } + } + + @Test + void fromIdentifierNegative() { + assertThat(WellKnownMimeType.fromIdentifier(-1)) + .isSameAs(WellKnownMimeType.fromIdentifier(-2)) + .isSameAs(WellKnownMimeType.fromIdentifier(-12)) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } + + @Test + void fromIdentifierReserved() { + assertThat(WellKnownMimeType.fromIdentifier(120)) + .isSameAs(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE); + } + + @Test + void fromStringUnknown() { + assertThat(WellKnownMimeType.fromString("foo/bar")) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } + + @Test + void fromStringUnknownReservedStillReturnsUnparseable() { + assertThat( + WellKnownMimeType.fromString(WellKnownMimeType.UNKNOWN_RESERVED_MIME_TYPE.getString())) + .isSameAs(WellKnownMimeType.UNPARSEABLE_MIME_TYPE); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java b/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java index 988bd523d..46e0f77f4 100644 --- a/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/util/NumberUtilsTest.java @@ -18,6 +18,8 @@ import static org.assertj.core.api.Assertions.*; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -158,4 +160,28 @@ void requireUnsignedShortOverFlow() { .isThrownBy(() -> NumberUtils.requireUnsignedShort(1 << 16)) .withMessage("%d is larger than 16 bits", 1 << 16); } + + @Test + void encodeUnsignedMedium() { + ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); + NumberUtils.encodeUnsignedMedium(buffer, 129); + buffer.markReaderIndex(); + + assertThat(buffer.readUnsignedMedium()).as("reading as unsigned medium").isEqualTo(129); + + buffer.resetReaderIndex(); + assertThat(buffer.readMedium()).as("reading as signed medium").isEqualTo(129); + } + + @Test + void encodeUnsignedMediumLarge() { + ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); + NumberUtils.encodeUnsignedMedium(buffer, 0xFFFFFC); + buffer.markReaderIndex(); + + assertThat(buffer.readUnsignedMedium()).as("reading as unsigned medium").isEqualTo(16777212); + + buffer.resetReaderIndex(); + assertThat(buffer.readMedium()).as("reading as signed medium").isEqualTo(-4); + } } From 7a5f9ffb86355bedcde0e7033b6e277e59c52501 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Tue, 11 Jun 2019 16:01:46 -0700 Subject: [PATCH 054/181] update version Signed-off-by: Robert Roeser --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 5e14306b7..740dee4db 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.12.2-RC4-SNAPSHOT +version=0.12.2-RC4 From 09b85c3fc5f7ed79abf883eb22943db1414ed3af Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Tue, 11 Jun 2019 16:04:16 -0700 Subject: [PATCH 055/181] update version to 1.0.0-RC1-SNAPSHOT Signed-off-by: Robert Roeser --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 740dee4db..0f198822f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.12.2-RC4 +version=0.12.2-RC5-SNAPSHOT From 30542bdb8a0b3168fd472cde073a0db57767044c Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Tue, 11 Jun 2019 16:04:28 -0700 Subject: [PATCH 056/181] update version to 1.0.0-RC1-SNAPSHOT Signed-off-by: Robert Roeser --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 0f198822f..4ee96633a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=0.12.2-RC5-SNAPSHOT +version=1.0.0-SNAPSHOT From 8a1108de486e3c2bd29d36fc016368ae645c2b9c Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Tue, 11 Jun 2019 16:04:40 -0700 Subject: [PATCH 057/181] update version to 1.0.0-RC1-SNAPSHOT Signed-off-by: Robert Roeser --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 4ee96633a..c7ab3683b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-SNAPSHOT +version=1.0.0-RC1-SNAPSHOT From d47b747e875a39ffb0a3a17db5a9f97e26915f43 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Wed, 12 Jun 2019 22:19:21 +0300 Subject: [PATCH 058/181] ClientServerMultiplexer should properly route zero non-setup frames (#650) * ClientServerInputMultiplexer: fix routing Signed-off-by: Maksym Ostroverkhov * keep-alives aligned with new frames multiplexer Signed-off-by: Maksym Ostroverkhov * fix responder MetadataPush subscription Signed-off-by: Maksym Ostroverkhov * review Signed-off-by: Maksym Ostroverkhov --- .../main/java/io/rsocket/RSocketFactory.java | 14 +- .../java/io/rsocket/RSocketRequester.java | 3 +- .../java/io/rsocket/RSocketResponder.java | 60 ++---- .../ClientServerInputMultiplexer.java | 33 ++- .../test/java/io/rsocket/KeepAliveTest.java | 201 ++++++------------ .../ClientServerInputMultiplexerTest.java | 180 ++++++++++++++-- 6 files changed, 271 insertions(+), 220 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 1743bd6da..741a29611 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -300,7 +300,7 @@ public Mono start() { DuplexConnection wrappedConnection = clientSetup.connection(); ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(wrappedConnection, plugins); + new ClientServerInputMultiplexer(wrappedConnection, plugins, true); RSocketRequester rSocketRequester = new RSocketRequester( @@ -500,7 +500,7 @@ public Start transport(Supplier> tra private Mono acceptor(ServerSetup serverSetup, DuplexConnection connection) { ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(connection, plugins); + new ClientServerInputMultiplexer(connection, plugins, false); return multiplexer .asSetupConnection() @@ -553,7 +553,10 @@ private Mono acceptSetup( wrappedMultiplexer.asServerConnection(), payloadDecoder, errorConsumer, - StreamIdSupplier.serverSupplier()); + StreamIdSupplier.serverSupplier(), + setupPayload.keepAliveInterval(), + setupPayload.keepAliveMaxLifetime(), + keepAliveHandler); RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); @@ -571,10 +574,7 @@ private Mono acceptSetup( wrappedMultiplexer.asClientConnection(), wrappedRSocketHandler, payloadDecoder, - errorConsumer, - setupPayload.keepAliveInterval(), - setupPayload.keepAliveMaxLifetime(), - keepAliveHandler); + errorConsumer); }) .doFinally(signalType -> setupPayload.release()) .then(); diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java index 9c01db295..d2af9f14f 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java @@ -61,7 +61,6 @@ class RSocketRequester implements RSocket { private final ByteBufAllocator allocator; private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; - /*client requester*/ RSocketRequester( ByteBufAllocator allocator, DuplexConnection connection, @@ -100,7 +99,7 @@ class RSocketRequester implements RSocket { } } - /*server requester*/ + /*for testing only*/ RSocketRequester( ByteBufAllocator allocator, DuplexConnection connection, diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java index f861303a4..d77c125eb 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java @@ -16,22 +16,15 @@ package io.rsocket; -import static io.rsocket.keepalive.KeepAliveSupport.KeepAlive; -import static io.rsocket.keepalive.KeepAliveSupport.ServerKeepAliveSupport; - import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.ReferenceCountUtil; import io.netty.util.collection.IntObjectHashMap; import io.rsocket.exceptions.ApplicationErrorException; -import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.frame.*; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.LimitableRequestPublisher; import io.rsocket.internal.UnboundedProcessor; -import io.rsocket.keepalive.KeepAliveFramesAcceptor; -import io.rsocket.keepalive.KeepAliveHandler; -import io.rsocket.keepalive.KeepAliveSupport; import java.util.Collections; import java.util.Map; import java.util.function.Consumer; @@ -58,28 +51,13 @@ class RSocketResponder implements ResponderRSocket { private final UnboundedProcessor sendProcessor; private final ByteBufAllocator allocator; - private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; - /*client responder*/ RSocketResponder( ByteBufAllocator allocator, DuplexConnection connection, RSocket requestHandler, PayloadDecoder payloadDecoder, Consumer errorConsumer) { - this(allocator, connection, requestHandler, payloadDecoder, errorConsumer, 0, 0, null); - } - - /*server responder*/ - RSocketResponder( - ByteBufAllocator allocator, - DuplexConnection connection, - RSocket requestHandler, - PayloadDecoder payloadDecoder, - Consumer errorConsumer, - int keepAliveTickPeriod, - int keepAliveAckTimeout, - KeepAliveHandler keepAliveHandler) { this.allocator = allocator; this.connection = connection; @@ -112,22 +90,6 @@ class RSocketResponder implements ResponderRSocket { receiveDisposable.dispose(); }) .subscribe(null, errorConsumer); - - if (keepAliveTickPeriod != 0 && keepAliveHandler != null) { - KeepAliveSupport keepAliveSupport = - new ServerKeepAliveSupport(allocator, keepAliveTickPeriod, keepAliveAckTimeout); - keepAliveFramesAcceptor = - keepAliveHandler.start(keepAliveSupport, sendProcessor::onNext, this::terminate); - } else { - keepAliveFramesAcceptor = null; - } - } - - private void terminate(KeepAlive keepAlive) { - String message = - String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()); - errorConsumer.accept(new ConnectionErrorException(message)); - connection.dispose(); } private void handleSendProcessorError(Throwable t) { @@ -309,9 +271,6 @@ private void handleFrame(ByteBuf frame) { case CANCEL: handleCancelFrame(streamId); break; - case KEEPALIVE: - handleKeepAliveFrame(frame); - break; case REQUEST_N: handleRequestN(streamId, frame); break; @@ -326,7 +285,7 @@ private void handleFrame(ByteBuf frame) { handleChannel(streamId, channelPayload, channelInitialRequestN); break; case METADATA_PUSH: - metadataPush(payloadDecoder.apply(frame)); + handleMetadataPush(metadataPush(payloadDecoder.apply(frame))); break; case PAYLOAD: // TODO: Hook in receiving socket. @@ -517,10 +476,19 @@ private void handleChannel(int streamId, Payload payload, int initialRequestN) { } } - private void handleKeepAliveFrame(ByteBuf frame) { - if (keepAliveFramesAcceptor != null) { - keepAliveFramesAcceptor.receive(frame); - } + private void handleMetadataPush(Mono result) { + result.subscribe( + new BaseSubscriber() { + @Override + protected void hookOnSubscribe(Subscription subscription) { + subscription.request(Long.MAX_VALUE); + } + + @Override + protected void hookOnError(Throwable throwable) { + errorConsumer.accept(throwable); + } + }); } private void handleCancelFrame(int streamId) { diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index e69c6631c..1e76b6898 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -54,10 +54,11 @@ public class ClientServerInputMultiplexer implements Closeable { private final DuplexConnection clientServerConnection; public ClientServerInputMultiplexer(DuplexConnection source) { - this(source, emptyPluginRegistry); + this(source, emptyPluginRegistry, false); } - public ClientServerInputMultiplexer(DuplexConnection source, PluginRegistry plugins) { + public ClientServerInputMultiplexer( + DuplexConnection source, PluginRegistry plugins, boolean isClient) { this.source = source; final MonoProcessor> setup = MonoProcessor.create(); final MonoProcessor> server = MonoProcessor.create(); @@ -79,10 +80,19 @@ public ClientServerInputMultiplexer(DuplexConnection source, PluginRegistry plug int streamId = FrameHeaderFlyweight.streamId(frame); final Type type; if (streamId == 0) { - if (isSetup(frame)) { - type = Type.SETUP; - } else { - type = Type.CLIENT; + switch (FrameHeaderFlyweight.frameType(frame)) { + case SETUP: + case RESUME: + case RESUME_OK: + type = Type.SETUP; + break; + case LEASE: + case KEEPALIVE: + case ERROR: + type = isClient ? Type.CLIENT : Type.SERVER; + break; + default: + type = isClient ? Type.SERVER : Type.CLIENT; } } else if ((streamId & 0b1) == 0) { type = Type.SERVER; @@ -144,17 +154,6 @@ public Mono onClose() { return source.onClose(); } - private static boolean isSetup(ByteBuf frame) { - switch (FrameHeaderFlyweight.frameType(frame)) { - case SETUP: - case RESUME: - case RESUME_OK: - return true; - default: - return false; - } - } - private static class InternalDuplexConnection implements DuplexConnection { private final DuplexConnection source; private final MonoProcessor>[] processors; diff --git a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java index 106e61097..2119d66f7 100644 --- a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java @@ -34,12 +34,9 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Stream; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; @@ -49,112 +46,58 @@ public class KeepAliveTest { private static final int KEEP_ALIVE_TIMEOUT = 1000; private static final int RESUMABLE_KEEP_ALIVE_TIMEOUT = 200; - static Stream> rSocketStates() { - return Stream.of( - requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT), - responder(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT)); + private RSocketState requesterState; + private ResumableRSocketState resumableRequesterState; + + static RSocketState requester(int tickPeriod, int timeout) { + TestDuplexConnection connection = new TestDuplexConnection(); + Errors errors = new Errors(); + RSocketRequester rSocket = + new RSocketRequester( + ByteBufAllocator.DEFAULT, + connection, + DefaultPayload::create, + errors, + StreamIdSupplier.clientSupplier(), + tickPeriod, + timeout, + new DefaultKeepAliveHandler(connection)); + return new RSocketState(rSocket, errors, connection); } - static Stream> resumableRSocketStates() { - return Stream.of( - resumableRequester(KEEP_ALIVE_INTERVAL, RESUMABLE_KEEP_ALIVE_TIMEOUT), - resumableResponder(KEEP_ALIVE_INTERVAL, RESUMABLE_KEEP_ALIVE_TIMEOUT)); + static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { + TestDuplexConnection connection = new TestDuplexConnection(); + ResumableDuplexConnection resumableConnection = + new ResumableDuplexConnection( + "test", + connection, + new InMemoryResumableFramesStore("test", 10_000), + Duration.ofSeconds(10), + false); + + Errors errors = new Errors(); + RSocketRequester rSocket = + new RSocketRequester( + ByteBufAllocator.DEFAULT, + resumableConnection, + DefaultPayload::create, + errors, + StreamIdSupplier.clientSupplier(), + tickPeriod, + timeout, + new ResumableKeepAliveHandler(resumableConnection)); + return new ResumableRSocketState(rSocket, errors, connection, resumableConnection); } - static Supplier requester(int tickPeriod, int timeout) { - return () -> { - TestDuplexConnection connection = new TestDuplexConnection(); - Errors errors = new Errors(); - RSocketRequester rSocket = - new RSocketRequester( - ByteBufAllocator.DEFAULT, - connection, - DefaultPayload::create, - errors, - StreamIdSupplier.clientSupplier(), - tickPeriod, - timeout, - new DefaultKeepAliveHandler(connection)); - return new RSocketState(rSocket, errors, connection); - }; + @BeforeEach + void setUp() { + requesterState = requester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); + resumableRequesterState = resumableRequester(KEEP_ALIVE_INTERVAL, RESUMABLE_KEEP_ALIVE_TIMEOUT); } - static Supplier responder(int tickPeriod, int timeout) { - return () -> { - TestDuplexConnection connection = new TestDuplexConnection(); - AbstractRSocket handler = new AbstractRSocket() {}; - Errors errors = new Errors(); - RSocketResponder rSocket = - new RSocketResponder( - ByteBufAllocator.DEFAULT, - connection, - handler, - DefaultPayload::create, - errors, - tickPeriod, - timeout, - new DefaultKeepAliveHandler(connection)); - return new RSocketState(rSocket, errors, connection); - }; - } - - static Supplier resumableRequester(int tickPeriod, int timeout) { - return () -> { - TestDuplexConnection connection = new TestDuplexConnection(); - ResumableDuplexConnection resumableConnection = - new ResumableDuplexConnection( - "test", - connection, - new InMemoryResumableFramesStore("test", 10_000), - Duration.ofSeconds(10), - false); - - Errors errors = new Errors(); - RSocketRequester rSocket = - new RSocketRequester( - ByteBufAllocator.DEFAULT, - resumableConnection, - DefaultPayload::create, - errors, - StreamIdSupplier.clientSupplier(), - tickPeriod, - timeout, - new ResumableKeepAliveHandler(resumableConnection)); - return new ResumableRSocketState(rSocket, errors, connection, resumableConnection); - }; - } - - static Supplier resumableResponder(int tickPeriod, int timeout) { - return () -> { - AbstractRSocket handler = new AbstractRSocket() {}; - TestDuplexConnection connection = new TestDuplexConnection(); - ResumableDuplexConnection resumableConnection = - new ResumableDuplexConnection( - "test", - connection, - new InMemoryResumableFramesStore("test", 10_000), - Duration.ofSeconds(10), - false); - Errors errors = new Errors(); - RSocketResponder rSocket = - new RSocketResponder( - ByteBufAllocator.DEFAULT, - resumableConnection, - handler, - DefaultPayload::create, - errors, - tickPeriod, - timeout, - new ResumableKeepAliveHandler(resumableConnection)); - return new ResumableRSocketState(rSocket, errors, connection, resumableConnection); - }; - } - - @ParameterizedTest - @MethodSource("rSocketStates") - void rSocketNotDisposedOnPresentKeepAlives(Supplier testDataSupplier) { - RSocketState RSocketState = testDataSupplier.get(); - TestDuplexConnection connection = RSocketState.connection(); + @Test + void rSocketNotDisposedOnPresentKeepAlives() { + TestDuplexConnection connection = requesterState.connection(); Flux.interval(Duration.ofMillis(100)) .subscribe( @@ -165,33 +108,30 @@ void rSocketNotDisposedOnPresentKeepAlives(Supplier testDataSuppli Mono.delay(Duration.ofMillis(1500)).block(); - RSocket rSocket = RSocketState.rSocket(); - List errors = RSocketState.errors().errors(); + RSocket rSocket = requesterState.rSocket(); + List errors = requesterState.errors().errors(); Assertions.assertThat(rSocket.isDisposed()).isFalse(); Assertions.assertThat(errors).isEmpty(); } - @ParameterizedTest - @MethodSource("rSocketStates") - void noKeepAlivesSentAfterRSocketDispose(Supplier testDataSupplier) { - RSocketState RSocketState = testDataSupplier.get(); - RSocketState.rSocket().dispose(); + @Test + void noKeepAlivesSentAfterRSocketDispose() { + requesterState.rSocket().dispose(); StepVerifier.create( - Flux.from(RSocketState.connection().getSentAsPublisher()).take(Duration.ofMillis(500))) + Flux.from(requesterState.connection().getSentAsPublisher()) + .take(Duration.ofMillis(500))) .expectComplete() .verify(Duration.ofSeconds(1)); } - @ParameterizedTest - @MethodSource("rSocketStates") - void rSocketDisposedOnMissingKeepAlives(Supplier testDataSupplier) { - RSocketState rSocketState = testDataSupplier.get(); - RSocket rSocket = rSocketState.rSocket(); + @Test + void rSocketDisposedOnMissingKeepAlives() { + RSocket rSocket = requesterState.rSocket(); Mono.delay(Duration.ofMillis(1500)).block(); - List errors = rSocketState.errors().errors(); + List errors = requesterState.errors().errors(); Assertions.assertThat(rSocket.isDisposed()).isTrue(); Assertions.assertThat(errors).hasSize(1); Throwable throwable = errors.get(0); @@ -200,7 +140,7 @@ void rSocketDisposedOnMissingKeepAlives(Supplier testDataSupplier) @Test void clientRequesterSendsKeepAlives() { - RSocketState RSocketState = requester(100, 1000).get(); + RSocketState RSocketState = requester(100, 1000); TestDuplexConnection connection = RSocketState.connection(); StepVerifier.create(Flux.from(connection.getSentAsPublisher()).take(3)) @@ -212,8 +152,8 @@ void clientRequesterSendsKeepAlives() { } @Test - void serverResponderSendsKeepAlives() { - RSocketState RSocketState = responder(100, 1000).get(); + void requesterRespondsToKeepAlives() { + RSocketState RSocketState = requester(100_000, 100_000); TestDuplexConnection connection = RSocketState.connection(); Mono.delay(Duration.ofMillis(100)) .subscribe( @@ -231,7 +171,7 @@ void serverResponderSendsKeepAlives() { @Test void resumableRequesterNoKeepAlivesAfterDisconnect() { ResumableRSocketState rSocketState = - resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT).get(); + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); TestDuplexConnection testConnection = rSocketState.connection(); ResumableDuplexConnection resumableDuplexConnection = rSocketState.resumableDuplexConnection(); @@ -245,7 +185,7 @@ void resumableRequesterNoKeepAlivesAfterDisconnect() { @Test void resumableRequesterKeepAlivesAfterReconnect() { ResumableRSocketState rSocketState = - resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT).get(); + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); ResumableDuplexConnection resumableDuplexConnection = rSocketState.resumableDuplexConnection(); resumableDuplexConnection.disconnect(); TestDuplexConnection newTestConnection = new TestDuplexConnection(); @@ -261,7 +201,7 @@ void resumableRequesterKeepAlivesAfterReconnect() { @Test void resumableRequesterNoKeepAlivesAfterDispose() { ResumableRSocketState rSocketState = - resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT).get(); + resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); rSocketState.rSocket().dispose(); StepVerifier.create( Flux.from(rSocketState.connection().getSentAsPublisher()).take(Duration.ofMillis(500))) @@ -269,14 +209,11 @@ void resumableRequesterNoKeepAlivesAfterDispose() { .verify(Duration.ofSeconds(5)); } - @ParameterizedTest - @MethodSource("resumableRSocketStates") - void resumableRSocketsNotDisposedOnMissingKeepAlives( - Supplier testDataSupplier) { - ResumableRSocketState rSocketState = testDataSupplier.get(); - RSocket rSocket = rSocketState.rSocket(); - List errors = rSocketState.errors().errors(); - TestDuplexConnection connection = rSocketState.connection(); + @Test + void resumableRSocketsNotDisposedOnMissingKeepAlives() { + RSocket rSocket = resumableRequesterState.rSocket(); + List errors = resumableRequesterState.errors().errors(); + TestDuplexConnection connection = resumableRequesterState.connection(); Mono.delay(Duration.ofMillis(500)).block(); diff --git a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java index 3b03429a6..ea3b2d57f 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java @@ -18,8 +18,10 @@ import static org.junit.Assert.assertEquals; +import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.netty.buffer.Unpooled; +import io.rsocket.frame.*; import io.rsocket.plugins.PluginRegistry; import io.rsocket.test.util.TestDuplexConnection; import java.util.concurrent.atomic.AtomicInteger; @@ -28,50 +30,196 @@ public class ClientServerInputMultiplexerTest { private TestDuplexConnection source; - private ClientServerInputMultiplexer multiplexer; + private ClientServerInputMultiplexer clientMultiplexer; private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + private ClientServerInputMultiplexer serverMultiplexer; @Before public void setup() { source = new TestDuplexConnection(); - multiplexer = new ClientServerInputMultiplexer(source, new PluginRegistry()); + clientMultiplexer = new ClientServerInputMultiplexer(source, new PluginRegistry(), true); + serverMultiplexer = new ClientServerInputMultiplexer(source, new PluginRegistry(), false); } @Test - public void testSplits() { + public void clientSplits() { AtomicInteger clientFrames = new AtomicInteger(); AtomicInteger serverFrames = new AtomicInteger(); - AtomicInteger connectionFrames = new AtomicInteger(); + AtomicInteger setupFrames = new AtomicInteger(); - multiplexer + clientMultiplexer .asClientConnection() .receive() .doOnNext(f -> clientFrames.incrementAndGet()) .subscribe(); - multiplexer + clientMultiplexer .asServerConnection() .receive() .doOnNext(f -> serverFrames.incrementAndGet()) .subscribe(); - multiplexer + clientMultiplexer .asSetupConnection() .receive() - .doOnNext(f -> connectionFrames.incrementAndGet()) + .doOnNext(f -> setupFrames.incrementAndGet()) .subscribe(); - source.addToReceivedBuffer(ErrorFrameFlyweight.encode(allocator, 1, new Exception())); + source.addToReceivedBuffer(errorFrame(1)); assertEquals(1, clientFrames.get()); assertEquals(0, serverFrames.get()); - assertEquals(0, connectionFrames.get()); + assertEquals(0, setupFrames.get()); - source.addToReceivedBuffer(ErrorFrameFlyweight.encode(allocator, 2, new Exception())); - assertEquals(1, clientFrames.get()); + source.addToReceivedBuffer(errorFrame(1)); + assertEquals(2, clientFrames.get()); + assertEquals(0, serverFrames.get()); + assertEquals(0, setupFrames.get()); + + source.addToReceivedBuffer(leaseFrame()); + assertEquals(3, clientFrames.get()); + assertEquals(0, serverFrames.get()); + assertEquals(0, setupFrames.get()); + + source.addToReceivedBuffer(keepAliveFrame()); + assertEquals(4, clientFrames.get()); + assertEquals(0, serverFrames.get()); + assertEquals(0, setupFrames.get()); + + source.addToReceivedBuffer(errorFrame(2)); + assertEquals(4, clientFrames.get()); assertEquals(1, serverFrames.get()); - assertEquals(0, connectionFrames.get()); + assertEquals(0, setupFrames.get()); + + source.addToReceivedBuffer(errorFrame(0)); + assertEquals(5, clientFrames.get()); + assertEquals(1, serverFrames.get()); + assertEquals(0, setupFrames.get()); + + source.addToReceivedBuffer(metadataPushFrame()); + assertEquals(5, clientFrames.get()); + assertEquals(2, serverFrames.get()); + assertEquals(0, setupFrames.get()); + + source.addToReceivedBuffer(setupFrame()); + assertEquals(5, clientFrames.get()); + assertEquals(2, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(resumeFrame()); + assertEquals(5, clientFrames.get()); + assertEquals(2, serverFrames.get()); + assertEquals(2, setupFrames.get()); + + source.addToReceivedBuffer(resumeOkFrame()); + assertEquals(5, clientFrames.get()); + assertEquals(2, serverFrames.get()); + assertEquals(3, setupFrames.get()); + } + + @Test + public void serverSplits() { + AtomicInteger clientFrames = new AtomicInteger(); + AtomicInteger serverFrames = new AtomicInteger(); + AtomicInteger setupFrames = new AtomicInteger(); + + serverMultiplexer + .asClientConnection() + .receive() + .doOnNext(f -> clientFrames.incrementAndGet()) + .subscribe(); + serverMultiplexer + .asServerConnection() + .receive() + .doOnNext(f -> serverFrames.incrementAndGet()) + .subscribe(); + serverMultiplexer + .asSetupConnection() + .receive() + .doOnNext(f -> setupFrames.incrementAndGet()) + .subscribe(); - source.addToReceivedBuffer(ErrorFrameFlyweight.encode(allocator, 1, new Exception())); + source.addToReceivedBuffer(errorFrame(1)); + assertEquals(1, clientFrames.get()); + assertEquals(0, serverFrames.get()); + assertEquals(0, setupFrames.get()); + + source.addToReceivedBuffer(errorFrame(1)); + assertEquals(2, clientFrames.get()); + assertEquals(0, serverFrames.get()); + assertEquals(0, setupFrames.get()); + + source.addToReceivedBuffer(leaseFrame()); assertEquals(2, clientFrames.get()); assertEquals(1, serverFrames.get()); - assertEquals(0, connectionFrames.get()); + assertEquals(0, setupFrames.get()); + + source.addToReceivedBuffer(keepAliveFrame()); + assertEquals(2, clientFrames.get()); + assertEquals(2, serverFrames.get()); + assertEquals(0, setupFrames.get()); + + source.addToReceivedBuffer(errorFrame(2)); + assertEquals(2, clientFrames.get()); + assertEquals(3, serverFrames.get()); + assertEquals(0, setupFrames.get()); + + source.addToReceivedBuffer(errorFrame(0)); + assertEquals(2, clientFrames.get()); + assertEquals(4, serverFrames.get()); + assertEquals(0, setupFrames.get()); + + source.addToReceivedBuffer(metadataPushFrame()); + assertEquals(3, clientFrames.get()); + assertEquals(4, serverFrames.get()); + assertEquals(0, setupFrames.get()); + + source.addToReceivedBuffer(setupFrame()); + assertEquals(3, clientFrames.get()); + assertEquals(4, serverFrames.get()); + assertEquals(1, setupFrames.get()); + + source.addToReceivedBuffer(resumeFrame()); + assertEquals(3, clientFrames.get()); + assertEquals(4, serverFrames.get()); + assertEquals(2, setupFrames.get()); + + source.addToReceivedBuffer(resumeOkFrame()); + assertEquals(3, clientFrames.get()); + assertEquals(4, serverFrames.get()); + assertEquals(3, setupFrames.get()); + } + + private ByteBuf resumeFrame() { + return ResumeFrameFlyweight.encode(allocator, Unpooled.EMPTY_BUFFER, 0, 0); + } + + private ByteBuf setupFrame() { + return SetupFrameFlyweight.encode( + ByteBufAllocator.DEFAULT, + false, + 0, + 42, + "application/octet-stream", + "application/octet-stream", + Unpooled.EMPTY_BUFFER, + Unpooled.EMPTY_BUFFER); + } + + private ByteBuf leaseFrame() { + return LeaseFlyweight.encode(allocator, 1_000, 1, Unpooled.EMPTY_BUFFER); + } + + private ByteBuf errorFrame(int i) { + return ErrorFrameFlyweight.encode(allocator, i, new Exception()); + } + + private ByteBuf resumeOkFrame() { + return ResumeOkFrameFlyweight.encode(allocator, 0); + } + + private ByteBuf keepAliveFrame() { + return KeepAliveFrameFlyweight.encode(allocator, false, 0, Unpooled.EMPTY_BUFFER); + } + + private ByteBuf metadataPushFrame() { + return MetadataPushFrameFlyweight.encode(allocator, Unpooled.EMPTY_BUFFER); } } From c9a4ea174c138dc8e9aa2f3852d07053f2f736dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 14 Jun 2019 00:12:01 -0400 Subject: [PATCH 059/181] Fix CompositeMetadataFlyweight check of ASCII only custom mime (#651) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix CompositeMetadataFlyweight check of ASCII only custom mime This fixes potential false positives in the encodeMetadataHeader method "ASCII only" check that results in IllegalArgumentException. The first byte is readable but not actually written when performing the check, yet we would consider it in `isText` (looking at a random byte value). Signed-off-by: Simon Baslé * add a test for the ascii check bug Signed-off-by: Simon Baslé * forgot googleDataFormat (again) Signed-off-by: Simon Baslé --- .../metadata/CompositeMetadataFlyweight.java | 4 +- .../CompositeMetadataFlyweightTest.java | 45 +++++++++++++++---- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index 9abd638cb..0520285c2 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -329,13 +329,15 @@ static ByteBuf encodeMetadataHeader( ByteBufAllocator allocator, String customMime, int metadataLength) { ByteBuf metadataHeader = allocator.buffer(4 + customMime.length()); // reserve 1 byte for the customMime length + // /!\ careful not to read that first byte, which is random at this point int writerIndexInitial = metadataHeader.writerIndex(); metadataHeader.writerIndex(writerIndexInitial + 1); // write the custom mime in UTF8 but validate it is all ASCII-compatible // (which produces the right result since ASCII chars are still encoded on 1 byte in UTF8) int customMimeLength = ByteBufUtil.writeUtf8(metadataHeader, customMime); - if (!ByteBufUtil.isText(metadataHeader, CharsetUtil.US_ASCII)) { + if (!ByteBufUtil.isText( + metadataHeader, metadataHeader.readerIndex() + 1, customMimeLength, CharsetUtil.US_ASCII)) { metadataHeader.release(); throw new IllegalArgumentException("custom mime type must be US_ASCII characters only"); } diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java index 1a22e9e23..bd5e4295a 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataFlyweightTest.java @@ -19,15 +19,9 @@ import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeAndContentBuffersSlices; import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeIdFromMimeBuffer; import static io.rsocket.metadata.CompositeMetadataFlyweight.decodeMimeTypeFromMimeBuffer; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.assertj.core.api.Assertions.assertThatIllegalStateException; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.CompositeByteBuf; -import io.netty.buffer.Unpooled; -import io.netty.buffer.UnpooledByteBufAllocator; +import static org.assertj.core.api.Assertions.*; + +import io.netty.buffer.*; import io.netty.util.CharsetUtil; import io.rsocket.test.util.ByteBufUtils; import io.rsocket.util.NumberUtils; @@ -524,4 +518,37 @@ void knownMimeHeaderZero_avro() { assertThat(content.readableBytes()).as("no metadata content").isZero(); } + + @Test + void encodeCustomHeaderAsciiCheckSkipsFirstByte() { + final ByteBuf badBuf = Unpooled.copiedBuffer("é00000000000", CharsetUtil.UTF_8); + badBuf.writerIndex(0); + assertThat(badBuf.readerIndex()).isZero(); + + ByteBufAllocator allocator = + new AbstractByteBufAllocator() { + @Override + public boolean isDirectBufferPooled() { + return false; + } + + @Override + protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) { + return badBuf; + } + + @Override + protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) { + return badBuf; + } + }; + + assertThatCode( + () -> CompositeMetadataFlyweight.encodeMetadataHeader(allocator, "custom/type", 0)) + .doesNotThrowAnyException(); + + assertThat(badBuf.readByte()).isEqualTo((byte) 10); + assertThat(badBuf.readCharSequence(11, CharsetUtil.UTF_8)).hasToString("custom/type"); + assertThat(badBuf.readUnsignedMedium()).isEqualTo(0); + } } From 19f0c7809b9ab339f038bf28badad852ebf0da92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Basl=C3=A9?= Date: Fri, 14 Jun 2019 00:12:23 -0400 Subject: [PATCH 060/181] Expose a Stream out of CompositeMetadata (#649) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Simon Baslé --- .../rsocket/metadata/CompositeMetadata.java | 21 +++++++++++++++++++ .../metadata/CompositeMetadataTest.java | 21 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java index 9eb349396..4a48921b1 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadata.java @@ -28,6 +28,10 @@ import io.netty.buffer.CompositeByteBuf; import io.rsocket.metadata.CompositeMetadata.Entry; import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import reactor.util.annotation.Nullable; /** @@ -62,6 +66,23 @@ public CompositeMetadata(ByteBuf source, boolean retainSlices) { this.retainSlices = retainSlices; } + /** + * Turn this {@link CompositeMetadata} into a sequential {@link Stream}. + * + * @return the composite metadata sequential {@link Stream} + */ + public Stream stream() { + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + iterator(), Spliterator.DISTINCT | Spliterator.NONNULL | Spliterator.ORDERED), + false); + } + + /** + * An {@link Iterator} that lazily decodes {@link Entry} in this composite metadata. + * + * @return the composite metadata {@link Iterator} + */ @Override public Iterator iterator() { return new Iterator() { diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java index cc00df7d4..f06bdcc0c 100644 --- a/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java +++ b/rsocket-core/src/test/java/io/rsocket/metadata/CompositeMetadataTest.java @@ -31,6 +31,7 @@ import io.rsocket.test.util.ByteBufUtils; import io.rsocket.util.NumberUtils; import java.util.Iterator; +import java.util.Spliterator; import org.junit.jupiter.api.Test; class CompositeMetadataTest { @@ -154,4 +155,24 @@ void decodeThreeEntries() { assertThat(iterator.hasNext()).as("has no more than 3 entries").isFalse(); } + + @Test + void streamIsNotParallel() { + final CompositeMetadata metadata = + new CompositeMetadata(ByteBufUtils.getRandomByteBuf(5), false); + + assertThat(metadata.stream().isParallel()).as("isParallel").isFalse(); + } + + @Test + void streamSpliteratorCharacteristics() { + final CompositeMetadata metadata = + new CompositeMetadata(ByteBufUtils.getRandomByteBuf(5), false); + + assertThat(metadata.stream().spliterator()) + .matches(s -> s.hasCharacteristics(Spliterator.ORDERED), "ORDERED") + .matches(s -> s.hasCharacteristics(Spliterator.DISTINCT), "DISTINCT") + .matches(s -> s.hasCharacteristics(Spliterator.NONNULL), "NONNULL") + .matches(s -> !s.hasCharacteristics(Spliterator.SIZED), "not SIZED"); + } } From fb5aa1d27896c85ae2830b0da7ac382ba7010295 Mon Sep 17 00:00:00 2001 From: xiazuojie Date: Mon, 17 Jun 2019 15:35:14 +0800 Subject: [PATCH 061/181] fix: LoadBalancedRSocketMono and RSocketSupplierPool not in sync (#655) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: LoadBalancedRSocketMono#activeSockets and RSocketSupplierPool#leasedSuppliers are not in sync add: more logs to help debugging fix: log when factoryPool has new items Signed-off-by: 刘禅 * fix: format Signed-off-by: 刘禅 --- .../client/LoadBalancedRSocketMono.java | 51 +++++++++++++------ .../rsocket/client/RSocketSupplierPool.java | 12 ++++- 2 files changed, 45 insertions(+), 18 deletions(-) diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java index ba799f763..376dcdf73 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java @@ -279,7 +279,6 @@ private synchronized void addSockets(int numberOfNewSocket) { if (optional.isPresent()) { RSocketSupplier supplier = optional.get(); WeightedSocket socket = new WeightedSocket(supplier, lowerQuantile, higherQuantile); - activeSockets.add(socket); } else { break; } @@ -356,7 +355,8 @@ private synchronized void quickSlowestRS() { } if (slowest != null) { - activeSockets.remove(slowest); + logger.debug("Disposing slowest WeightedSocket {}", slowest); + slowest.dispose(); } } @@ -461,7 +461,7 @@ public synchronized String toString() { @Override public void dispose() { - synchronized (this) {; + synchronized (this) { activeSockets.forEach(WeightedSocket::dispose); activeSockets.clear(); onClose.onComplete(); @@ -573,12 +573,16 @@ private class WeightedSocket extends AbstractRSocket implements LoadBalancerSock this.interArrivalTime = new Ewma(1, TimeUnit.MINUTES, DEFAULT_INITIAL_INTER_ARRIVAL_TIME); this.pendingStreams = new AtomicLong(); + logger.debug("Creating WeightedSocket {} from factory {}", WeightedSocket.this, factory); + WeightedSocket.this .onClose() .doFinally( s -> { pool.accept(factory); activeSockets.remove(WeightedSocket.this); + logger.debug( + "Removed {} from factory {} from activeSockets", WeightedSocket.this, factory); refreshSockets(); }) .subscribe(); @@ -588,7 +592,11 @@ private class WeightedSocket extends AbstractRSocket implements LoadBalancerSock .retryBackoff(weightedSocketRetries, weightedSocketBackOff, weightedSocketMaxBackOff) .doOnError( throwable -> { - logger.error("error while connecting {}", throwable); + logger.error( + "error while connecting {} from factory {}", + WeightedSocket.this, + factory, + throwable); WeightedSocket.this.dispose(); }) .subscribe( @@ -598,7 +606,8 @@ private class WeightedSocket extends AbstractRSocket implements LoadBalancerSock .onClose() .doFinally( signalType -> { - System.out.println("RSocket closed"); + logger.info( + "RSocket {} from factory {} closed", WeightedSocket.this, factory); WeightedSocket.this.dispose(); }) .subscribe(); @@ -608,7 +617,7 @@ private class WeightedSocket extends AbstractRSocket implements LoadBalancerSock .onClose() .doFinally( signalType -> { - System.out.println("Factory closed"); + logger.info("Factory {} closed", factory); rSocket.dispose(); }) .subscribe(); @@ -618,20 +627,30 @@ private class WeightedSocket extends AbstractRSocket implements LoadBalancerSock .onClose() .doFinally( signalType -> { - System.out.println("WeightedSocket closed"); + logger.info( + "WeightedSocket {} from factory {} closed", + WeightedSocket.this, + factory); rSocket.dispose(); }) .subscribe(); - synchronized (LoadBalancedRSocketMono.this) { + /*synchronized (LoadBalancedRSocketMono.this) { if (activeSockets.size() >= targetAperture) { quickSlowestRS(); pendingSockets -= 1; } - } - + }*/ rSocketMono.onNext(rSocket); availability = 1.0; + if (!WeightedSocket.this + .isDisposed()) { // May be already disposed because of retryBackoff delay + activeSockets.add(WeightedSocket.this); + logger.debug( + "Added WeightedSocket {} from factory {} to activeSockets", + WeightedSocket.this, + factory); + } }); } @@ -829,11 +848,11 @@ public long lastTimeUsedMillis() { */ private class LatencySubscriber implements Subscriber { private final Subscriber child; - private final LoadBalancedRSocketMono.WeightedSocket socket; + private final WeightedSocket socket; private final AtomicBoolean done; private long start; - LatencySubscriber(Subscriber child, LoadBalancedRSocketMono.WeightedSocket socket) { + LatencySubscriber(Subscriber child, WeightedSocket socket) { this.child = child; this.socket = socket; this.done = new AtomicBoolean(false); @@ -893,9 +912,9 @@ public void onComplete() { */ private class CountingSubscriber implements Subscriber { private final Subscriber child; - private final LoadBalancedRSocketMono.WeightedSocket socket; + private final WeightedSocket socket; - CountingSubscriber(Subscriber child, LoadBalancedRSocketMono.WeightedSocket socket) { + CountingSubscriber(Subscriber child, WeightedSocket socket) { this.child = child; this.socket = socket; } @@ -916,8 +935,8 @@ public void onError(Throwable t) { socket.pendingStreams.decrementAndGet(); child.onError(t); if (t instanceof TransportException || t instanceof ClosedChannelException) { - activeSockets.remove(socket); - refreshSockets(); + logger.debug("Disposing {} from activeSockets because of error {}", socket, t); + socket.dispose(); } } diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/RSocketSupplierPool.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/RSocketSupplierPool.java index 35615d2a2..1683ee125 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/RSocketSupplierPool.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/RSocketSupplierPool.java @@ -84,6 +84,9 @@ private synchronized void handleNewFactories(Collection newFact } factoryPool.addAll(added); + if (!added.isEmpty()) { + changed = true; + } if (changed && logger.isDebugEnabled()) { StringBuilder msgBuilder = new StringBuilder(); @@ -104,8 +107,10 @@ private synchronized void handleNewFactories(Collection newFact @Override public synchronized void accept(RSocketSupplier rSocketSupplier) { - leasedSuppliers.remove(rSocketSupplier); - if (!rSocketSupplier.isDisposed()) { + boolean contained = leasedSuppliers.remove(rSocketSupplier); + if (contained + && !rSocketSupplier + .isDisposed()) { // only added leasedSupplier back to factoryPool if it's still there factoryPool.add(rSocketSupplier); } } @@ -119,6 +124,7 @@ public synchronized Optional get() { if (rSocketSupplier.availability() > 0.0) { factoryPool.remove(0); leasedSuppliers.add(rSocketSupplier); + logger.debug("Added {} to leasedSuppliers", rSocketSupplier); optional = Optional.of(rSocketSupplier); } } else if (poolSize > 1) { @@ -143,10 +149,12 @@ public synchronized Optional get() { if (factory0.availability() > factory1.availability()) { factoryPool.remove(i0); leasedSuppliers.add(factory0); + logger.debug("Added {} to leasedSuppliers", factory0); optional = Optional.of(factory0); } else { factoryPool.remove(i1); leasedSuppliers.add(factory1); + logger.debug("Added {} to leasedSuppliers", factory1); optional = Optional.of(factory1); } } From 265a83b12f75fdac3fb8a401feeb04aa614a21cd Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Mon, 17 Jun 2019 17:17:55 +0300 Subject: [PATCH 062/181] Lease support (#648) * lease support * rsocket requester: remove lifecycle, make interactions single subscriber only * wrap requester rsockets in multi subscription rsocket, except lease case * pluggable stats * single subscriber Requester: metadata-push, fnf, request-response requests are lazy; make sure above can be subscribed at most once, and send at most 1 request frame Signed-off-by: Maksym Ostroverkhov --- gradle.properties | 2 +- .../main/java/io/rsocket/RSocketFactory.java | 126 +++- .../java/io/rsocket/RSocketRequester.java | 561 +++++++++--------- .../java/io/rsocket/RSocketResponder.java | 47 +- .../exceptions/MissingLeaseException.java | 36 ++ .../rsocket/exceptions/RejectedException.java | 2 +- .../main/java/io/rsocket/frame/FrameUtil.java | 2 +- ...lyweight.java => LeaseFrameFlyweight.java} | 2 +- .../src/main/java/io/rsocket/lease/Lease.java | 52 +- .../main/java/io/rsocket/lease/LeaseImpl.java | 110 +++- .../java/io/rsocket/lease/LeaseStats.java | 28 + .../main/java/io/rsocket/lease/Leases.java | 65 ++ .../rsocket/lease/RequesterLeaseHandler.java | 114 ++++ .../rsocket/lease/ResponderLeaseHandler.java | 151 +++++ .../rsocket/util/MultiSubscriberRSocket.java | 54 ++ .../java/io/rsocket/util/OnceConsumer.java | 33 ++ .../test/java/io/rsocket/KeepAliveTest.java | 11 +- .../java/io/rsocket/RSocketLeaseTest.java | 324 ++++++++++ .../RSocketRequesterSubscribersTest.java | 120 ++++ .../java/io/rsocket/RSocketRequesterTest.java | 11 +- .../java/io/rsocket/RSocketResponderTest.java | 4 +- .../src/test/java/io/rsocket/RSocketTest.java | 11 +- .../java/io/rsocket/SetupRejectionTest.java | 13 +- ...Test.java => LeaseFrameFlyweightTest.java} | 19 +- .../ClientServerInputMultiplexerTest.java | 2 +- .../java/io/rsocket/lease/LeaseImplTest.java | 86 +++ .../test/util/TestClientTransport.java | 19 + .../test/util/TestServerTransport.java | 46 ++ .../transport/tcp/lease/LeaseExample.java | 148 +++++ .../main/java/io/rsocket/test/TestFrames.java | 2 +- 30 files changed, 1833 insertions(+), 368 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/exceptions/MissingLeaseException.java rename rsocket-core/src/main/java/io/rsocket/frame/{LeaseFlyweight.java => LeaseFrameFlyweight.java} (98%) create mode 100644 rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java create mode 100644 rsocket-core/src/main/java/io/rsocket/lease/Leases.java create mode 100644 rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java create mode 100644 rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java create mode 100644 rsocket-core/src/main/java/io/rsocket/util/MultiSubscriberRSocket.java create mode 100644 rsocket-core/src/main/java/io/rsocket/util/OnceConsumer.java create mode 100644 rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/RSocketRequesterSubscribersTest.java rename rsocket-core/src/test/java/io/rsocket/frame/{LeaseFlyweightTest.java => LeaseFrameFlyweightTest.java} (51%) create mode 100644 rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java create mode 100644 rsocket-core/src/test/java/io/rsocket/test/util/TestServerTransport.java create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java diff --git a/gradle.properties b/gradle.properties index c7ab3683b..14a39e27b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-RC1-SNAPSHOT +version=1.0.0-RC1-LEASE-SNAPSHOT diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 741a29611..5e152fbc7 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -31,6 +31,7 @@ import io.rsocket.internal.ClientSetup; import io.rsocket.internal.ServerSetup; import io.rsocket.keepalive.KeepAliveHandler; +import io.rsocket.lease.*; import io.rsocket.plugins.DuplexConnectionInterceptor; import io.rsocket.plugins.PluginRegistry; import io.rsocket.plugins.Plugins; @@ -40,12 +41,10 @@ import io.rsocket.transport.ServerTransport; import io.rsocket.util.ConnectionUtils; import io.rsocket.util.EmptyPayload; +import io.rsocket.util.MultiSubscriberRSocket; import java.time.Duration; import java.util.Objects; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; +import java.util.function.*; import reactor.core.publisher.Mono; /** Factory for creating RSocket clients and servers. */ @@ -92,6 +91,8 @@ default Start transport(ServerTransport transport) { } public static class ClientRSocketFactory implements ClientTransportAcceptor { + private static final String CLIENT_TAG = "client"; + private Supplier> acceptor = () -> rSocket -> new AbstractRSocket() {}; @@ -115,13 +116,17 @@ public static class ClientRSocketFactory implements ClientTransportAcceptor { private boolean resumeCleanupStoreOnKeepAlive; private Supplier resumeTokenSupplier = ResumeFrameFlyweight::generateResumeToken; private Function resumeStoreFactory = - token -> new InMemoryResumableFramesStore("client", 100_000); + token -> new InMemoryResumableFramesStore(CLIENT_TAG, 100_000); private Duration resumeSessionDuration = Duration.ofMinutes(2); private Duration resumeStreamTimeout = Duration.ofSeconds(10); private Supplier resumeStrategySupplier = () -> new ExponentialBackoffResumeStrategy(Duration.ofSeconds(1), Duration.ofSeconds(16), 2); + private boolean multiSubscriberRequester = true; + private boolean leaseEnabled; + private Supplier> leasesSupplier = Leases::new; + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; public ClientRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { @@ -205,6 +210,22 @@ public ClientRSocketFactory metadataMimeType(String metadataMimeType) { return this; } + public ClientRSocketFactory lease(Supplier> leasesSupplier) { + this.leaseEnabled = true; + this.leasesSupplier = Objects.requireNonNull(leasesSupplier); + return this; + } + + public ClientRSocketFactory lease() { + this.leaseEnabled = true; + return this; + } + + public ClientRSocketFactory singleSubscriberRequester() { + this.multiSubscriberRequester = false; + return this; + } + public ClientRSocketFactory resume() { this.resumeEnabled = true; return this; @@ -302,7 +323,14 @@ public Mono start() { ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(wrappedConnection, plugins, true); - RSocketRequester rSocketRequester = + boolean isLeaseEnabled = leaseEnabled; + Leases leases = leasesSupplier.get(); + RequesterLeaseHandler requesterLeaseHandler = + isLeaseEnabled + ? new RequesterLeaseHandler.Impl(CLIENT_TAG, leases.receiver()) + : RequesterLeaseHandler.None; + + RSocket rSocketRequester = new RSocketRequester( allocator, multiplexer.asClientConnection(), @@ -311,12 +339,17 @@ public Mono start() { StreamIdSupplier.clientSupplier(), keepAliveTickPeriod(), keepAliveTimeout(), - keepAliveHandler); + keepAliveHandler, + requesterLeaseHandler); + + if (multiSubscriberRequester) { + rSocketRequester = new MultiSubscriberRSocket(rSocketRequester); + } ByteBuf setupFrame = SetupFrameFlyweight.encode( allocator, - false, + isLeaseEnabled, keepAliveTickPeriod(), keepAliveTimeout(), resumeToken, @@ -337,13 +370,20 @@ public Mono start() { RSocket wrappedRSocketHandler = plugins.applyResponder(rSocketHandler); - RSocketResponder rSocketResponder = + ResponderLeaseHandler responderLeaseHandler = + isLeaseEnabled + ? new ResponderLeaseHandler.Impl<>( + CLIENT_TAG, allocator, leases.sender(), errorConsumer, leases.stats()) + : ResponderLeaseHandler.None; + + RSocket rSocketResponder = new RSocketResponder( allocator, multiplexer.asServerConnection(), wrappedRSocketHandler, payloadDecoder, - errorConsumer); + errorConsumer, + responderLeaseHandler); return wrappedConnection.sendOne(setupFrame).thenReturn(wrappedRSocketRequester); }); @@ -382,16 +422,23 @@ private Mono newConnection() { } public static class ServerRSocketFactory { + private static final String SERVER_TAG = "server"; + private SocketAcceptor acceptor; private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; private Consumer errorConsumer = Throwable::printStackTrace; private int mtu = 0; private PluginRegistry plugins = new PluginRegistry(Plugins.defaultPlugins()); + private boolean resumeSupported; private Duration resumeSessionDuration = Duration.ofSeconds(120); private Duration resumeStreamTimeout = Duration.ofSeconds(10); private Function resumeStoreFactory = - token -> new InMemoryResumableFramesStore("server", 100_000); + token -> new InMemoryResumableFramesStore(SERVER_TAG, 100_000); + + private boolean multiSubscriberRequester = true; + private boolean leaseEnabled; + private Supplier> leasesSupplier = Leases::new; private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; private boolean resumeCleanupStoreOnKeepAlive; @@ -450,6 +497,22 @@ public ServerRSocketFactory errorConsumer(Consumer errorConsumer) { return this; } + public ServerRSocketFactory lease(Supplier> leasesSupplier) { + this.leaseEnabled = true; + this.leasesSupplier = Objects.requireNonNull(leasesSupplier); + return this; + } + + public ServerRSocketFactory lease() { + this.leaseEnabled = true; + return this; + } + + public ServerRSocketFactory singleSubscriberRequester() { + this.multiSubscriberRequester = false; + return this; + } + public ServerRSocketFactory resume() { this.resumeSupported = true; return this; @@ -541,13 +604,31 @@ private Mono acceptSetup( multiplexer.dispose(); }); } + + boolean isLeaseEnabled = leaseEnabled; + + if (SetupFrameFlyweight.honorLease(setupFrame) && !isLeaseEnabled) { + return sendError(multiplexer, new InvalidSetupException("lease is not supported")) + .doFinally( + signalType -> { + setupFrame.release(); + multiplexer.dispose(); + }); + } + return serverSetup.acceptRSocketSetup( setupFrame, multiplexer, (keepAliveHandler, wrappedMultiplexer) -> { ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(setupFrame); - RSocketRequester rSocketRequester = + Leases leases = leasesSupplier.get(); + RequesterLeaseHandler requesterLeaseHandler = + isLeaseEnabled + ? new RequesterLeaseHandler.Impl(SERVER_TAG, leases.receiver()) + : RequesterLeaseHandler.None; + + RSocket rSocketRequester = new RSocketRequester( allocator, wrappedMultiplexer.asServerConnection(), @@ -556,8 +637,12 @@ private Mono acceptSetup( StreamIdSupplier.serverSupplier(), setupPayload.keepAliveInterval(), setupPayload.keepAliveMaxLifetime(), - keepAliveHandler); + keepAliveHandler, + requesterLeaseHandler); + if (multiSubscriberRequester) { + rSocketRequester = new MultiSubscriberRSocket(rSocketRequester); + } RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); return acceptor @@ -568,13 +653,24 @@ private Mono acceptSetup( rSocketHandler -> { RSocket wrappedRSocketHandler = plugins.applyResponder(rSocketHandler); - RSocketResponder rSocketResponder = + ResponderLeaseHandler responderLeaseHandler = + isLeaseEnabled + ? new ResponderLeaseHandler.Impl<>( + SERVER_TAG, + allocator, + leases.sender(), + errorConsumer, + leases.stats()) + : ResponderLeaseHandler.None; + + RSocket rSocketResponder = new RSocketResponder( allocator, wrappedMultiplexer.asClientConnection(), wrappedRSocketHandler, payloadDecoder, - errorConsumer); + errorConsumer, + responderLeaseHandler); }) .doFinally(signalType -> setupPayload.release()) .then(); diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java index d2af9f14f..4c2d4e45e 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java @@ -27,28 +27,33 @@ import io.rsocket.exceptions.Exceptions; import io.rsocket.frame.*; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.LimitableRequestPublisher; -import io.rsocket.internal.UnboundedProcessor; -import io.rsocket.internal.UnicastMonoProcessor; +import io.rsocket.internal.*; import io.rsocket.keepalive.KeepAliveFramesAcceptor; import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.keepalive.KeepAliveSupport; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.util.OnceConsumer; import java.nio.channels.ClosedChannelException; import java.util.Collections; import java.util.Map; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; import java.util.function.LongConsumer; -import java.util.function.Supplier; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; import org.reactivestreams.Processor; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import reactor.core.publisher.*; /** * Requester Side of a RSocket socket. Sends {@link ByteBuf}s to a {@link RSocketResponder} of peer */ class RSocketRequester implements RSocket { + private static final AtomicReferenceFieldUpdater TERMINATION_ERROR = + AtomicReferenceFieldUpdater.newUpdater( + RSocketRequester.class, Throwable.class, "terminationError"); private final DuplexConnection connection; private final PayloadDecoder payloadDecoder; @@ -57,9 +62,10 @@ class RSocketRequester implements RSocket { private final Map senders; private final Map> receivers; private final UnboundedProcessor sendProcessor; - private final Lifecycle lifecycle = new Lifecycle(); + private final RequesterLeaseHandler leaseHandler; private final ByteBufAllocator allocator; private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; + private volatile Throwable terminationError; RSocketRequester( ByteBufAllocator allocator, @@ -69,12 +75,14 @@ class RSocketRequester implements RSocket { StreamIdSupplier streamIdSupplier, int keepAliveTickPeriod, int keepAliveAckTimeout, - KeepAliveHandler keepAliveHandler) { + @Nullable KeepAliveHandler keepAliveHandler, + RequesterLeaseHandler leaseHandler) { this.allocator = allocator; this.connection = connection; this.payloadDecoder = payloadDecoder; this.errorConsumer = errorConsumer; this.streamIdSupplier = streamIdSupplier; + this.leaseHandler = leaseHandler; this.senders = Collections.synchronizedMap(new IntObjectHashMap<>()); this.receivers = Collections.synchronizedMap(new IntObjectHashMap<>()); @@ -99,27 +107,17 @@ class RSocketRequester implements RSocket { } } - /*for testing only*/ - RSocketRequester( - ByteBufAllocator allocator, - DuplexConnection connection, - PayloadDecoder payloadDecoder, - Consumer errorConsumer, - StreamIdSupplier streamIdSupplier) { - this(allocator, connection, payloadDecoder, errorConsumer, streamIdSupplier, 0, 0, null); - } - private void terminate(KeepAlive keepAlive) { String message = String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()); ConnectionErrorException err = new ConnectionErrorException(message); - lifecycle.setTerminationError(err); + setTerminationError(err); errorConsumer.accept(err); connection.dispose(); } private void handleSendProcessorError(Throwable t) { - Throwable terminationError = lifecycle.getTerminationError(); + Throwable terminationError = this.terminationError; Throwable err = terminationError != null ? terminationError : t; receivers .values() @@ -181,7 +179,7 @@ public Mono metadataPush(Payload payload) { @Override public double availability() { - return connection.availability(); + return Math.min(connection.availability(), leaseHandler.availability()); } @Override @@ -200,233 +198,281 @@ public Mono onClose() { } private Mono handleFireAndForget(Payload payload) { - return lifecycle.active( - () -> { - final int streamId = streamIdSupplier.nextStreamId(); - ByteBuf requestFrame = - RequestFireAndForgetFrameFlyweight.encode( - allocator, - streamId, - false, - payload.hasMetadata() ? payload.sliceMetadata().retain() : null, - payload.sliceData().retain()); - payload.release(); - sendProcessor.onNext(requestFrame); - }); + Throwable err = checkAvailable(); + if (err != null) { + payload.release(); + return Mono.error(err); + } + + final int streamId = streamIdSupplier.nextStreamId(); + + return emptyUnicastMono() + .doOnSubscribe( + new OnceConsumer() { + @Override + public void acceptOnce(@Nonnull Subscription subscription) { + ByteBuf requestFrame = + RequestFireAndForgetFrameFlyweight.encode( + allocator, + streamId, + false, + payload.hasMetadata() ? payload.sliceMetadata().retain() : null, + payload.sliceData().retain()); + payload.release(); + + sendProcessor.onNext(requestFrame); + } + }); } private Mono handleRequestResponse(final Payload payload) { - return lifecycle.activeMono( - () -> { - int streamId = streamIdSupplier.nextStreamId(); - final UnboundedProcessor sendProcessor = this.sendProcessor; - final ByteBuf requestFrame = - RequestResponseFrameFlyweight.encode( - allocator, - streamId, - false, - payload.sliceMetadata().retain(), - payload.sliceData().retain()); - - payload.release(); - - UnicastMonoProcessor receiver = UnicastMonoProcessor.create(); - receivers.put(streamId, receiver); - - sendProcessor.onNext(requestFrame); - return receiver - .doOnError( - t -> sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t))) - .doFinally( - s -> { - if (s == SignalType.CANCEL) { - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - } - - receivers.remove(streamId); - }); - }); + Throwable err = checkAvailable(); + if (err != null) { + payload.release(); + return Mono.error(err); + } + + int streamId = streamIdSupplier.nextStreamId(); + final UnboundedProcessor sendProcessor = this.sendProcessor; + + UnicastMonoProcessor receiver = UnicastMonoProcessor.create(); + receivers.put(streamId, receiver); + + return receiver + .doOnSubscribe( + new OnceConsumer() { + @Override + public void acceptOnce(@Nonnull Subscription subscription) { + final ByteBuf requestFrame = + RequestResponseFrameFlyweight.encode( + allocator, + streamId, + false, + payload.sliceMetadata().retain(), + payload.sliceData().retain()); + payload.release(); + + sendProcessor.onNext(requestFrame); + } + }) + .doOnError(t -> sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t))) + .doFinally( + s -> { + if (s == SignalType.CANCEL) { + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + } + + receivers.remove(streamId); + }); } private Flux handleRequestStream(final Payload payload) { - return lifecycle.activeFlux( - () -> { - int streamId = streamIdSupplier.nextStreamId(); - - final UnboundedProcessor sendProcessor = this.sendProcessor; - final UnicastProcessor receiver = UnicastProcessor.create(); - - receivers.put(streamId, receiver); - - return receiver - .doOnRequest( - new LongConsumer() { - - // No need to make it atomic; See - // https://github.com/reactive-streams/reactive-streams-jvm#2.7 - boolean firstRequest = true; - - @Override - public void accept(long n) { - if (firstRequest && !receiver.isDisposed()) { - firstRequest = false; - sendProcessor.onNext( - RequestStreamFrameFlyweight.encode( - allocator, - streamId, - false, - n, - payload.sliceMetadata().retain(), - payload.sliceData().retain())); - payload.release(); - } else if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); - } - } - }) - .doOnError( - t -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t)); - } - }) - .doOnCancel( - () -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - } - }) - .doFinally(s -> receivers.remove(streamId)); - }); + Throwable err = checkAvailable(); + if (err != null) { + payload.release(); + return Flux.error(err); + } + + int streamId = streamIdSupplier.nextStreamId(); + + final UnboundedProcessor sendProcessor = this.sendProcessor; + final UnicastProcessor receiver = UnicastProcessor.create(); + + receivers.put(streamId, receiver); + + return receiver + .doOnRequest( + new LongConsumer() { + + boolean firstRequest = true; + + @Override + public void accept(long n) { + if (firstRequest && !receiver.isDisposed()) { + firstRequest = false; + sendProcessor.onNext( + RequestStreamFrameFlyweight.encode( + allocator, + streamId, + false, + n, + payload.sliceMetadata().retain(), + payload.sliceData().retain())); + payload.release(); + } else if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); + } + } + }) + .doOnError( + t -> { + if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t)); + } + }) + .doOnCancel( + () -> { + if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + } + }) + .doFinally(s -> receivers.remove(streamId)); } private Flux handleChannel(Flux request) { - return lifecycle.activeFlux( - () -> { - final UnboundedProcessor sendProcessor = this.sendProcessor; - final UnicastProcessor receiver = UnicastProcessor.create(); - final int streamId = streamIdSupplier.nextStreamId(); - - return receiver - .doOnRequest( - new LongConsumer() { - - // No need to make it atomic; See - // https://github.com/reactive-streams/reactive-streams-jvm#2.7 - boolean firstRequest = true; - - @Override - public void accept(long n) { - if (firstRequest) { - firstRequest = false; - request - .transform( - f -> { - LimitableRequestPublisher wrapped = - LimitableRequestPublisher.wrap(f); - // Need to set this to one for first the frame - wrapped.request(1); - senders.put(streamId, wrapped); - receivers.put(streamId, receiver); - - return wrapped; - }) - .subscribe( - new BaseSubscriber() { - - // no need to make it atomic; See - // https://github.com/reactive-streams/reactive-streams-jvm#1.3 - boolean firstPayload = true; - - @Override - protected void hookOnNext(Payload payload) { - final ByteBuf frame; - - if (firstPayload) { - firstPayload = false; - frame = - RequestChannelFrameFlyweight.encode( - allocator, - streamId, - false, - false, - n, - payload.sliceMetadata().retain(), - payload.sliceData().retain()); - } else { - frame = - PayloadFrameFlyweight.encode( - allocator, streamId, false, false, true, payload); - } - - sendProcessor.onNext(frame); - payload.release(); - } - - @Override - protected void hookOnComplete() { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext( - PayloadFrameFlyweight.encodeComplete( - allocator, streamId)); - } - if (firstPayload) { - receiver.onComplete(); - } - } - - @Override - protected void hookOnError(Throwable t) { - errorConsumer.accept(t); - receiver.dispose(); - } - }); - } else { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext( - RequestNFrameFlyweight.encode(allocator, streamId, n)); - } - } - } - }) - .doOnError( - t -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t)); - } - }) - .doOnCancel( - () -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - } - }) - .doFinally( - s -> { - receivers.remove(streamId); - LimitableRequestPublisher sender = senders.remove(streamId); - if (sender != null) { - sender.cancel(); - } - }); - }); + Throwable err = checkAvailable(); + if (err != null) { + return Flux.error(err); + } + + final UnboundedProcessor sendProcessor = this.sendProcessor; + final UnicastProcessor receiver = UnicastProcessor.create(); + final int streamId = streamIdSupplier.nextStreamId(); + + return receiver + .doOnRequest( + new LongConsumer() { + + boolean firstRequest = true; + + @Override + public void accept(long n) { + if (firstRequest) { + firstRequest = false; + request + .transform( + f -> { + LimitableRequestPublisher wrapped = + LimitableRequestPublisher.wrap(f); + // Need to set this to one for first the frame + wrapped.request(1); + senders.put(streamId, wrapped); + receivers.put(streamId, receiver); + + return wrapped; + }) + .subscribe( + new BaseSubscriber() { + + boolean firstPayload = true; + + @Override + protected void hookOnNext(Payload payload) { + final ByteBuf frame; + + if (firstPayload) { + firstPayload = false; + frame = + RequestChannelFrameFlyweight.encode( + allocator, + streamId, + false, + false, + n, + payload.sliceMetadata().retain(), + payload.sliceData().retain()); + } else { + frame = + PayloadFrameFlyweight.encode( + allocator, streamId, false, false, true, payload); + } + + sendProcessor.onNext(frame); + payload.release(); + } + + @Override + protected void hookOnComplete() { + if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext( + PayloadFrameFlyweight.encodeComplete(allocator, streamId)); + } + if (firstPayload) { + receiver.onComplete(); + } + } + + @Override + protected void hookOnError(Throwable t) { + errorConsumer.accept(t); + receiver.dispose(); + } + }); + } else { + if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); + } + } + } + }) + .doOnError( + t -> { + if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t)); + } + }) + .doOnCancel( + () -> { + if (contains(streamId) && !receiver.isDisposed()) { + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + } + }) + .doFinally( + s -> { + receivers.remove(streamId); + LimitableRequestPublisher sender = senders.remove(streamId); + if (sender != null) { + sender.cancel(); + } + }); } private Mono handleMetadataPush(Payload payload) { - return lifecycle.active( - () -> { - sendProcessor.onNext( - MetadataPushFrameFlyweight.encode(allocator, payload.sliceMetadata().retain())); - }); + Throwable err = this.terminationError; + if (err != null) { + payload.release(); + return Mono.error(err); + } + + return emptyUnicastMono() + .doOnSubscribe( + new OnceConsumer() { + @Override + public void acceptOnce(@Nonnull Subscription subscription) { + ByteBuf metadataPushFrame = + MetadataPushFrameFlyweight.encode(allocator, payload.sliceMetadata().retain()); + payload.release(); + + sendProcessor.onNext(metadataPushFrame); + } + }); + } + + private static UnicastMonoProcessor emptyUnicastMono() { + UnicastMonoProcessor result = UnicastMonoProcessor.create(); + result.onComplete(); + return result; + } + + private Throwable checkAvailable() { + Throwable err = this.terminationError; + if (err != null) { + return err; + } + RequesterLeaseHandler lh = leaseHandler; + if (!lh.useLease()) { + return lh.leaseError(); + } + return null; } private boolean contains(int streamId) { return receivers.containsKey(streamId); } - protected void terminate() { - lifecycle.setTerminationError(new ClosedChannelException()); - + private void terminate() { + setTerminationError(new ClosedChannelException()); + leaseHandler.dispose(); try { receivers.values().forEach(this::cleanUpSubscriber); senders.values().forEach(this::cleanUpLimitableRequestPublisher); @@ -437,6 +483,10 @@ protected void terminate() { } } + private void setTerminationError(Throwable error) { + TERMINATION_ERROR.compareAndSet(this, null, error); + } + private synchronized void cleanUpLimitableRequestPublisher( LimitableRequestPublisher limitableRequestPublisher) { try { @@ -448,7 +498,7 @@ private synchronized void cleanUpLimitableRequestPublisher( private synchronized void cleanUpSubscriber(Processor subscriber) { try { - subscriber.onError(lifecycle.getTerminationError()); + subscriber.onError(terminationError); } catch (Throwable t) { errorConsumer.accept(t); } @@ -474,11 +524,12 @@ private void handleStreamZero(FrameType type, ByteBuf frame) { switch (type) { case ERROR: RuntimeException error = Exceptions.from(frame); - lifecycle.setTerminationError(error); + setTerminationError(error); errorConsumer.accept(error); connection.dispose(); break; case LEASE: + leaseHandler.receive(frame); break; case KEEPALIVE: if (keepAliveFramesAcceptor != null) { @@ -561,54 +612,4 @@ private void handleMissingResponseProcessor(int streamId, FrameType type, ByteBu // receiving a frame after a given stream has been cancelled/completed, // so ignore (cancellation is async so there is a race condition) } - - private static class Lifecycle { - - private static final AtomicReferenceFieldUpdater TERMINATION_ERROR = - AtomicReferenceFieldUpdater.newUpdater( - Lifecycle.class, Throwable.class, "terminationError"); - private volatile Throwable terminationError; - - public Mono active(Runnable runnable) { - return Mono.create( - sink -> { - if (terminationError == null) { - runnable.run(); - sink.success(); - } else { - sink.error(terminationError); - } - }); - } - - public Mono activeMono(Supplier> supplier) { - return Mono.defer( - () -> { - if (terminationError == null) { - return supplier.get(); - } else { - return Mono.error(terminationError); - } - }); - } - - public Flux activeFlux(Supplier> supplier) { - return Flux.defer( - () -> { - if (terminationError == null) { - return supplier.get(); - } else { - return Flux.error(terminationError); - } - }); - } - - public Throwable getTerminationError() { - return terminationError; - } - - public void setTerminationError(Throwable err) { - TERMINATION_ERROR.compareAndSet(this, null, err); - } - } } diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java index d77c125eb..932ab70a2 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java @@ -25,6 +25,7 @@ import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.LimitableRequestPublisher; import io.rsocket.internal.UnboundedProcessor; +import io.rsocket.lease.ResponderLeaseHandler; import java.util.Collections; import java.util.Map; import java.util.function.Consumer; @@ -44,6 +45,7 @@ class RSocketResponder implements ResponderRSocket { private final ResponderRSocket responderRSocket; private final PayloadDecoder payloadDecoder; private final Consumer errorConsumer; + private final ResponderLeaseHandler leaseHandler; private final Map sendingLimitableSubscriptions; private final Map sendingSubscriptions; @@ -57,7 +59,8 @@ class RSocketResponder implements ResponderRSocket { DuplexConnection connection, RSocket requestHandler, PayloadDecoder payloadDecoder, - Consumer errorConsumer) { + Consumer errorConsumer, + ResponderLeaseHandler leaseHandler) { this.allocator = allocator; this.connection = connection; @@ -67,6 +70,7 @@ class RSocketResponder implements ResponderRSocket { this.payloadDecoder = payloadDecoder; this.errorConsumer = errorConsumer; + this.leaseHandler = leaseHandler; this.sendingLimitableSubscriptions = Collections.synchronizedMap(new IntObjectHashMap<>()); this.sendingSubscriptions = Collections.synchronizedMap(new IntObjectHashMap<>()); this.channelProcessors = Collections.synchronizedMap(new IntObjectHashMap<>()); @@ -81,6 +85,7 @@ class RSocketResponder implements ResponderRSocket { .subscribe(null, this::handleSendProcessorError); Disposable receiveDisposable = connection.receive().subscribe(this::handleFrame, errorConsumer); + Disposable sendLeaseDisposable = leaseHandler.send(sendProcessor::onNext); this.connection .onClose() @@ -88,6 +93,7 @@ class RSocketResponder implements ResponderRSocket { s -> { cleanup(); receiveDisposable.dispose(); + sendLeaseDisposable.dispose(); }) .subscribe(null, errorConsumer); } @@ -169,7 +175,12 @@ private void handleSendProcessorCancel(SignalType t) { @Override public Mono fireAndForget(Payload payload) { try { - return requestHandler.fireAndForget(payload); + if (leaseHandler.useLease()) { + return requestHandler.fireAndForget(payload); + } else { + payload.release(); + return Mono.error(leaseHandler.leaseError()); + } } catch (Throwable t) { return Mono.error(t); } @@ -178,7 +189,12 @@ public Mono fireAndForget(Payload payload) { @Override public Mono requestResponse(Payload payload) { try { - return requestHandler.requestResponse(payload); + if (leaseHandler.useLease()) { + return requestHandler.requestResponse(payload); + } else { + payload.release(); + return Mono.error(leaseHandler.leaseError()); + } } catch (Throwable t) { return Mono.error(t); } @@ -187,7 +203,12 @@ public Mono requestResponse(Payload payload) { @Override public Flux requestStream(Payload payload) { try { - return requestHandler.requestStream(payload); + if (leaseHandler.useLease()) { + return requestHandler.requestStream(payload); + } else { + payload.release(); + return Flux.error(leaseHandler.leaseError()); + } } catch (Throwable t) { return Flux.error(t); } @@ -196,7 +217,11 @@ public Flux requestStream(Payload payload) { @Override public Flux requestChannel(Publisher payloads) { try { - return requestHandler.requestChannel(payloads); + if (leaseHandler.useLease()) { + return requestHandler.requestChannel(payloads); + } else { + return Flux.error(leaseHandler.leaseError()); + } } catch (Throwable t) { return Flux.error(t); } @@ -205,7 +230,12 @@ public Flux requestChannel(Publisher payloads) { @Override public Flux requestChannel(Payload payload, Publisher payloads) { try { - return responderRSocket.requestChannel(payload, payloads); + if (leaseHandler.useLease()) { + return responderRSocket.requestChannel(payload, payloads); + } else { + payload.release(); + return Flux.error(leaseHandler.leaseError()); + } } catch (Throwable t) { return Flux.error(t); } @@ -290,10 +320,6 @@ private void handleFrame(ByteBuf frame) { case PAYLOAD: // TODO: Hook in receiving socket. break; - case LEASE: - // Lease must not be received here as this is the server end of the socket which sends - // leases. - break; case NEXT: receiver = channelProcessors.get(streamId); if (receiver != null) { @@ -322,6 +348,7 @@ private void handleFrame(ByteBuf frame) { case SETUP: handleError(streamId, new IllegalStateException("Setup frame received post setup.")); break; + case LEASE: default: handleError( streamId, diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/MissingLeaseException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/MissingLeaseException.java new file mode 100644 index 000000000..4bd6ffb99 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/MissingLeaseException.java @@ -0,0 +1,36 @@ +package io.rsocket.exceptions; + +import io.rsocket.lease.Lease; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class MissingLeaseException extends RejectedException { + private static final long serialVersionUID = -6169748673403858959L; + + public MissingLeaseException(@Nonnull Lease lease, @Nonnull String tag) { + super(leaseMessage(Objects.requireNonNull(lease), Objects.requireNonNull(tag))); + } + + public MissingLeaseException(@Nonnull String tag) { + super(leaseMessage(null, Objects.requireNonNull(tag))); + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } + + static String leaseMessage(@Nullable Lease lease, String tag) { + if (lease == null) { + return String.format("[%s] Missing leases", tag); + } + if (lease.isEmpty()) { + return String.format("[%s] Lease was not received yet", tag); + } + boolean expired = lease.isExpired(); + int allowedRequests = lease.getAllowedRequests(); + return String.format( + "[%s] Missing leases. Expired: %b, allowedRequests: %d", tag, expired, allowedRequests); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java index e7556e19a..4ab83182e 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java @@ -26,7 +26,7 @@ * @see Error * Codes */ -public final class RejectedException extends RSocketException implements Retryable { +public class RejectedException extends RSocketException implements Retryable { private static final long serialVersionUID = 3926231092835143715L; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java index a4d862135..0d2175fb6 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java @@ -65,7 +65,7 @@ private static ByteBuf getMetadata(ByteBuf frame, FrameType frameType) { metadata = SetupFrameFlyweight.metadata(frame); break; case LEASE: - metadata = LeaseFlyweight.metadata(frame); + metadata = LeaseFrameFlyweight.metadata(frame); break; default: return Unpooled.EMPTY_BUFFER; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java similarity index 98% rename from rsocket-core/src/main/java/io/rsocket/frame/LeaseFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java index 527cdfafb..4676f4c9d 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java @@ -5,7 +5,7 @@ import io.netty.buffer.Unpooled; import javax.annotation.Nullable; -public class LeaseFlyweight { +public class LeaseFrameFlyweight { public static ByteBuf encode( final ByteBufAllocator allocator, diff --git a/rsocket-core/src/main/java/io/rsocket/lease/Lease.java b/rsocket-core/src/main/java/io/rsocket/lease/Lease.java index 62ce16907..b9d99f88a 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/Lease.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/Lease.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2019 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. @@ -17,10 +17,21 @@ package io.rsocket.lease; import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.rsocket.Availability; +import javax.annotation.Nonnull; import javax.annotation.Nullable; /** A contract for RSocket lease, which is sent by a request acceptor and is time bound. */ -public interface Lease { +public interface Lease extends Availability { + + static Lease create(int timeToLiveMillis, int numberOfRequests, @Nullable ByteBuf metadata) { + return LeaseImpl.create(timeToLiveMillis, numberOfRequests, metadata); + } + + static Lease create(int timeToLiveMillis, int numberOfRequests) { + return create(timeToLiveMillis, numberOfRequests, Unpooled.EMPTY_BUFFER); + } /** * Number of requests allowed by this lease. @@ -30,11 +41,30 @@ public interface Lease { int getAllowedRequests(); /** - * Number of seconds that this lease is valid from the time it is received. + * Initial number of requests allowed by this lease. + * + * @return initial number of requests allowed by this lease. + */ + default int getStartingAllowedRequests() { + throw new UnsupportedOperationException("Not implemented"); + } + + /** + * Number of milliseconds that this lease is valid from the time it is received. * - * @return Number of seconds that this lease is valid from the time it is received. + * @return Number of milliseconds that this lease is valid from the time it is received. */ - int getTtl(); + int getTimeToLiveMillis(); + + /** + * Number of milliseconds that this lease is still valid from now. + * + * @param now millis since epoch + * @return Number of milliseconds that this lease is still valid from now, or 0 if expired. + */ + default int getRemainingTimeToLiveMillis(long now) { + return isEmpty() ? 0 : (int) Math.max(0, expiry() - now); + } /** * Absolute time since epoch at which this lease will expire. @@ -48,7 +78,7 @@ public interface Lease { * * @return Metadata for the lease. */ - @Nullable + @Nonnull ByteBuf getMetadata(); /** @@ -69,4 +99,14 @@ default boolean isExpired() { default boolean isExpired(long now) { return now > expiry(); } + + /** Checks if the lease has not expired and there are allowed requests available */ + default boolean isValid() { + return !isExpired() && getAllowedRequests() > 0; + } + + /** Checks if the lease is empty(default value if no lease was received yet) */ + default boolean isEmpty() { + return getAllowedRequests() == 0 && getTimeToLiveMillis() == 0; + } } diff --git a/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java b/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java index 010afcda7..63b0433cb 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * Copyright 2015-2019 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. @@ -17,42 +17,53 @@ package io.rsocket.lease; import io.netty.buffer.ByteBuf; -import io.rsocket.frame.LeaseFlyweight; -import reactor.util.annotation.Nullable; +import io.netty.buffer.Unpooled; +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; -public final class LeaseImpl implements Lease { - - private final int allowedRequests; - private final int ttl; +public class LeaseImpl implements Lease { + private final int timeToLiveMillis; + private final AtomicInteger allowedRequests; + private final int startingAllowedRequests; + private final ByteBuf metadata; private final long expiry; - private final @Nullable ByteBuf metadata; - public LeaseImpl(int allowedRequests, int ttl) { - this(allowedRequests, ttl, null); + static LeaseImpl create(int timeToLiveMillis, int numberOfRequests, @Nullable ByteBuf metadata) { + assertLease(timeToLiveMillis, numberOfRequests); + return new LeaseImpl(timeToLiveMillis, numberOfRequests, metadata); + } + + static LeaseImpl empty() { + return new LeaseImpl(0, 0, null); } - public LeaseImpl(int allowedRequests, int ttl, ByteBuf metadata) { - this.allowedRequests = allowedRequests; - this.ttl = ttl; - expiry = System.currentTimeMillis() + ttl; - this.metadata = metadata; + private LeaseImpl(int timeToLiveMillis, int allowedRequests, @Nullable ByteBuf metadata) { + this.allowedRequests = new AtomicInteger(allowedRequests); + this.startingAllowedRequests = allowedRequests; + this.timeToLiveMillis = timeToLiveMillis; + this.metadata = metadata == null ? Unpooled.EMPTY_BUFFER : metadata; + this.expiry = timeToLiveMillis == 0 ? 0 : now() + timeToLiveMillis; } - public LeaseImpl(ByteBuf leaseFrame) { - this( - LeaseFlyweight.numRequests(leaseFrame), - LeaseFlyweight.ttl(leaseFrame), - LeaseFlyweight.metadata(leaseFrame)); + public int getTimeToLiveMillis() { + return timeToLiveMillis; } @Override public int getAllowedRequests() { - return allowedRequests; + return Math.max(0, allowedRequests.get()); + } + + @Override + public int getStartingAllowedRequests() { + return startingAllowedRequests; } + @Nonnull @Override - public int getTtl() { - return ttl; + public ByteBuf getMetadata() { + return metadata; } @Override @@ -61,19 +72,56 @@ public long expiry() { } @Override - public ByteBuf getMetadata() { - return metadata; + public boolean isValid() { + return !isEmpty() && getAllowedRequests() > 0 && !isExpired(); + } + + /** + * try use 1 allowed request of Lease + * + * @return true if used successfully, false if Lease is expired or no allowed requests available + */ + public boolean use() { + if (isExpired()) { + return false; + } + int remaining = + allowedRequests.accumulateAndGet(1, (cur, update) -> Math.max(-1, cur - update)); + return remaining >= 0; + } + + @Override + public double availability() { + return isValid() ? getAllowedRequests() / (double) getStartingAllowedRequests() : 0.0; } @Override public String toString() { + long now = now(); return "LeaseImpl{" - + "allowedRequests=" - + allowedRequests - + ", ttl=" - + ttl - + ", expiry=" - + expiry + + "timeToLiveMillis=" + + timeToLiveMillis + + ", allowedRequests=" + + getAllowedRequests() + + ", startingAllowedRequests=" + + startingAllowedRequests + + ", expired=" + + isExpired(now) + + ", remainingTimeToLiveMillis=" + + getRemainingTimeToLiveMillis(now) + '}'; } + + private static long now() { + return System.currentTimeMillis(); + } + + private static void assertLease(int timeToLiveMillis, int numberOfRequests) { + if (numberOfRequests <= 0) { + throw new IllegalArgumentException("Number of requests must be positive"); + } + if (timeToLiveMillis <= 0) { + throw new IllegalArgumentException("Time-to-live must be positive"); + } + } } diff --git a/rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java b/rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java new file mode 100644 index 000000000..791f5a023 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/lease/LeaseStats.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015-2019 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.lease; + +public interface LeaseStats { + + void onEvent(EventType eventType); + + enum EventType { + ACCEPT, + REJECT, + TERMINATE + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/lease/Leases.java b/rsocket-core/src/main/java/io/rsocket/lease/Leases.java new file mode 100644 index 000000000..4c90e38ce --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/lease/Leases.java @@ -0,0 +1,65 @@ +/* + * Copyright 2015-2019 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.lease; + +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import reactor.core.publisher.Flux; + +public class Leases { + private static final Function> noopLeaseSender = leaseStats -> Flux.never(); + private static final Consumer> noopLeaseReceiver = leases -> {}; + + private Function> leaseSender = noopLeaseSender; + private Consumer> leaseReceiver = noopLeaseReceiver; + private Optional stats = Optional.empty(); + + public static Leases create() { + return new Leases<>(); + } + + public Leases sender(Function, Flux> leaseSender) { + this.leaseSender = leaseSender; + return this; + } + + public Leases receiver(Consumer> leaseReceiver) { + this.leaseReceiver = leaseReceiver; + return this; + } + + public Leases stats(T stats) { + this.stats = Optional.of(Objects.requireNonNull(stats)); + return this; + } + + @SuppressWarnings("unchecked") + public Function, Flux> sender() { + return (Function, Flux>) leaseSender; + } + + public Consumer> receiver() { + return leaseReceiver; + } + + @SuppressWarnings("unchecked") + public Optional stats() { + return (Optional) stats; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java b/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java new file mode 100644 index 000000000..ca2111e87 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java @@ -0,0 +1,114 @@ +/* + * Copyright 2015-2019 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.lease; + +import io.netty.buffer.ByteBuf; +import io.rsocket.Availability; +import io.rsocket.exceptions.MissingLeaseException; +import io.rsocket.frame.LeaseFrameFlyweight; +import java.util.function.Consumer; +import reactor.core.Disposable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.ReplayProcessor; + +public interface RequesterLeaseHandler extends Availability, Disposable { + + boolean useLease(); + + Exception leaseError(); + + void receive(ByteBuf leaseFrame); + + void dispose(); + + final class Impl implements RequesterLeaseHandler { + private final String tag; + private final ReplayProcessor receivedLease; + private volatile LeaseImpl currentLease = LeaseImpl.empty(); + + public Impl(String tag, Consumer> leaseReceiver) { + this.tag = tag; + receivedLease = ReplayProcessor.create(1); + leaseReceiver.accept(receivedLease); + } + + @Override + public boolean useLease() { + return currentLease.use(); + } + + @Override + public Exception leaseError() { + LeaseImpl l = this.currentLease; + String t = this.tag; + if (!l.isValid()) { + return new MissingLeaseException(l, t); + } else { + return new MissingLeaseException(t); + } + } + + @Override + public void receive(ByteBuf leaseFrame) { + int numberOfRequests = LeaseFrameFlyweight.numRequests(leaseFrame); + int timeToLiveMillis = LeaseFrameFlyweight.ttl(leaseFrame); + ByteBuf metadata = LeaseFrameFlyweight.metadata(leaseFrame); + LeaseImpl lease = LeaseImpl.create(timeToLiveMillis, numberOfRequests, metadata); + currentLease = lease; + receivedLease.onNext(lease); + } + + @Override + public void dispose() { + receivedLease.onComplete(); + } + + @Override + public boolean isDisposed() { + return receivedLease.isTerminated(); + } + + @Override + public double availability() { + return currentLease.availability(); + } + } + + RequesterLeaseHandler None = + new RequesterLeaseHandler() { + @Override + public boolean useLease() { + return true; + } + + @Override + public Exception leaseError() { + throw new AssertionError("Error not possible with NOOP leases handler"); + } + + @Override + public void receive(ByteBuf leaseFrame) {} + + @Override + public void dispose() {} + + @Override + public double availability() { + return 1.0; + } + }; +} diff --git a/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java b/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java new file mode 100644 index 000000000..c517a55c4 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java @@ -0,0 +1,151 @@ +/* + * Copyright 2015-2019 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.lease; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Availability; +import io.rsocket.exceptions.MissingLeaseException; +import io.rsocket.frame.LeaseFrameFlyweight; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import javax.annotation.Nullable; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.publisher.Flux; + +public interface ResponderLeaseHandler extends Availability { + + boolean useLease(); + + Exception leaseError(); + + Disposable send(Consumer leaseFrameSender); + + final class Impl implements ResponderLeaseHandler { + private volatile LeaseImpl currentLease = LeaseImpl.empty(); + private final String tag; + private final ByteBufAllocator allocator; + private final Function, Flux> leaseSender; + private final Consumer errorConsumer; + private final Optional leaseStatsOption; + private final T leaseStats; + + public Impl( + String tag, + ByteBufAllocator allocator, + Function, Flux> leaseSender, + Consumer errorConsumer, + Optional leaseStatsOption) { + this.tag = tag; + this.allocator = allocator; + this.leaseSender = leaseSender; + this.errorConsumer = errorConsumer; + this.leaseStatsOption = leaseStatsOption; + this.leaseStats = leaseStatsOption.orElse(null); + } + + @Override + public boolean useLease() { + boolean success = currentLease.use(); + onUseEvent(success, leaseStats); + return success; + } + + @Override + public Exception leaseError() { + LeaseImpl l = currentLease; + String t = tag; + if (!l.isValid()) { + return new MissingLeaseException(l, t); + } else { + return new MissingLeaseException(t); + } + } + + @Override + public Disposable send(Consumer leaseFrameSender) { + return leaseSender + .apply(leaseStatsOption) + .doOnTerminate(this::onTerminateEvent) + .subscribe( + lease -> { + currentLease = create(lease); + leaseFrameSender.accept(createLeaseFrame(lease)); + }, + errorConsumer); + } + + @Override + public double availability() { + return currentLease.availability(); + } + + private ByteBuf createLeaseFrame(Lease lease) { + return LeaseFrameFlyweight.encode( + allocator, lease.getTimeToLiveMillis(), lease.getAllowedRequests(), lease.getMetadata()); + } + + private void onTerminateEvent() { + T ls = leaseStats; + if (ls != null) { + ls.onEvent(LeaseStats.EventType.TERMINATE); + } + } + + private void onUseEvent(boolean success, @Nullable T ls) { + if (ls != null) { + LeaseStats.EventType eventType = + success ? LeaseStats.EventType.ACCEPT : LeaseStats.EventType.REJECT; + ls.onEvent(eventType); + } + } + + private static LeaseImpl create(Lease lease) { + if (lease instanceof LeaseImpl) { + return (LeaseImpl) lease; + } else { + return LeaseImpl.create( + lease.getTimeToLiveMillis(), lease.getAllowedRequests(), lease.getMetadata()); + } + } + } + + ResponderLeaseHandler None = + new ResponderLeaseHandler() { + @Override + public boolean useLease() { + return true; + } + + @Override + public Exception leaseError() { + throw new AssertionError("Error not possible with NOOP leases handler"); + } + + @Override + public Disposable send(Consumer leaseFrameSender) { + return Disposables.disposed(); + } + + @Override + public double availability() { + return 1.0; + } + }; +} diff --git a/rsocket-core/src/main/java/io/rsocket/util/MultiSubscriberRSocket.java b/rsocket-core/src/main/java/io/rsocket/util/MultiSubscriberRSocket.java new file mode 100644 index 000000000..c2db6c238 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/MultiSubscriberRSocket.java @@ -0,0 +1,54 @@ +/* + * Copyright 2015-2019 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.util; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class MultiSubscriberRSocket extends RSocketProxy { + public MultiSubscriberRSocket(RSocket source) { + super(source); + } + + @Override + public Mono fireAndForget(Payload payload) { + return Mono.defer(() -> super.fireAndForget(payload)); + } + + @Override + public Mono requestResponse(Payload payload) { + return Mono.defer(() -> super.requestResponse(payload)); + } + + @Override + public Flux requestStream(Payload payload) { + return Flux.defer(() -> super.requestStream(payload)); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.defer(() -> super.requestChannel(payloads)); + } + + @Override + public Mono metadataPush(Payload payload) { + return Mono.defer(() -> super.metadataPush(payload)); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/util/OnceConsumer.java b/rsocket-core/src/main/java/io/rsocket/util/OnceConsumer.java new file mode 100644 index 000000000..af4c038cc --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/OnceConsumer.java @@ -0,0 +1,33 @@ +/* + * Copyright 2015-2019 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.util; + +import java.util.function.Consumer; + +public abstract class OnceConsumer implements Consumer { + private boolean isFirst = true; + + @Override + public final void accept(T t) { + if (isFirst) { + isFirst = false; + acceptOnce(t); + } + } + + public abstract void acceptOnce(T t); +} diff --git a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java index 2119d66f7..b275ccc33 100644 --- a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java @@ -26,6 +26,7 @@ import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; import io.rsocket.frame.KeepAliveFrameFlyweight; +import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.resume.InMemoryResumableFramesStore; import io.rsocket.resume.ResumableDuplexConnection; import io.rsocket.test.util.TestDuplexConnection; @@ -61,7 +62,8 @@ static RSocketState requester(int tickPeriod, int timeout) { StreamIdSupplier.clientSupplier(), tickPeriod, timeout, - new DefaultKeepAliveHandler(connection)); + new DefaultKeepAliveHandler(connection), + RequesterLeaseHandler.None); return new RSocketState(rSocket, errors, connection); } @@ -85,7 +87,8 @@ static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { StreamIdSupplier.clientSupplier(), tickPeriod, timeout, - new ResumableKeepAliveHandler(resumableConnection)); + new ResumableKeepAliveHandler(resumableConnection), + RequesterLeaseHandler.None); return new ResumableRSocketState(rSocket, errors, connection, resumableConnection); } @@ -106,7 +109,7 @@ void rSocketNotDisposedOnPresentKeepAlives() { KeepAliveFrameFlyweight.encode( ByteBufAllocator.DEFAULT, true, 0, Unpooled.EMPTY_BUFFER))); - Mono.delay(Duration.ofMillis(1500)).block(); + Mono.delay(Duration.ofMillis(2000)).block(); RSocket rSocket = requesterState.rSocket(); List errors = requesterState.errors().errors(); @@ -129,7 +132,7 @@ void noKeepAlivesSentAfterRSocketDispose() { void rSocketDisposedOnMissingKeepAlives() { RSocket rSocket = requesterState.rSocket(); - Mono.delay(Duration.ofMillis(1500)).block(); + Mono.delay(Duration.ofMillis(2000)).block(); List errors = requesterState.errors().errors(); Assertions.assertThat(rSocket.isDisposed()).isTrue(); diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java new file mode 100644 index 000000000..368a69b9e --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java @@ -0,0 +1,324 @@ +/* + * Copyright 2015-2019 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; + +import static io.rsocket.frame.FrameType.ERROR; +import static io.rsocket.frame.FrameType.SETUP; +import static org.assertj.core.data.Offset.offset; +import static org.mockito.Mockito.*; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.rsocket.exceptions.Exceptions; +import io.rsocket.exceptions.MissingLeaseException; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.LeaseFrameFlyweight; +import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.ClientServerInputMultiplexer; +import io.rsocket.lease.*; +import io.rsocket.plugins.PluginRegistry; +import io.rsocket.test.util.TestClientTransport; +import io.rsocket.test.util.TestDuplexConnection; +import io.rsocket.test.util.TestServerTransport; +import io.rsocket.util.DefaultPayload; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.reactivestreams.Publisher; +import reactor.core.publisher.EmitterProcessor; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class RSocketLeaseTest { + private static final String TAG = "test"; + + private RSocket rSocketRequester; + private ResponderLeaseHandler responderLeaseHandler; + private ByteBufAllocator byteBufAllocator; + private TestDuplexConnection connection; + private RSocketResponder rSocketResponder; + + private EmitterProcessor leaseSender = EmitterProcessor.create(); + private Flux leaseReceiver; + private RequesterLeaseHandler requesterLeaseHandler; + + @BeforeEach + void setUp() { + connection = new TestDuplexConnection(); + PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; + byteBufAllocator = UnpooledByteBufAllocator.DEFAULT; + + requesterLeaseHandler = new RequesterLeaseHandler.Impl(TAG, leases -> leaseReceiver = leases); + responderLeaseHandler = + new ResponderLeaseHandler.Impl<>( + TAG, byteBufAllocator, stats -> leaseSender, err -> {}, Optional.empty()); + + ClientServerInputMultiplexer multiplexer = + new ClientServerInputMultiplexer(connection, new PluginRegistry(), true); + rSocketRequester = + new RSocketRequester( + byteBufAllocator, + multiplexer.asClientConnection(), + payloadDecoder, + err -> {}, + StreamIdSupplier.clientSupplier(), + 0, + 0, + null, + requesterLeaseHandler); + + RSocket mockRSocketHandler = mock(RSocket.class); + when(mockRSocketHandler.metadataPush(any())).thenReturn(Mono.empty()); + when(mockRSocketHandler.fireAndForget(any())).thenReturn(Mono.empty()); + when(mockRSocketHandler.requestResponse(any())).thenReturn(Mono.empty()); + when(mockRSocketHandler.requestStream(any())).thenReturn(Flux.empty()); + when(mockRSocketHandler.requestChannel(any())).thenReturn(Flux.empty()); + + rSocketResponder = + new RSocketResponder( + byteBufAllocator, + multiplexer.asServerConnection(), + mockRSocketHandler, + payloadDecoder, + err -> {}, + responderLeaseHandler); + } + + @Test + public void serverRSocketFactoryRejectsUnsupportedLease() { + Payload payload = DefaultPayload.create(DefaultPayload.EMPTY_BUFFER); + ByteBuf setupFrame = + SetupFrameFlyweight.encode( + ByteBufAllocator.DEFAULT, + true, + 1000, + 30_000, + "application/octet-stream", + "application/octet-stream", + payload.sliceMetadata(), + payload.sliceData()); + + TestServerTransport transport = new TestServerTransport(); + Closeable server = + RSocketFactory.receive() + .acceptor((setup, sendingSocket) -> Mono.just(new AbstractRSocket() {})) + .transport(transport) + .start() + .block(); + + TestDuplexConnection connection = transport.connect(); + connection.addToReceivedBuffer(setupFrame); + + Collection sent = connection.getSent(); + Assertions.assertThat(sent).hasSize(1); + ByteBuf error = sent.iterator().next(); + Assertions.assertThat(FrameHeaderFlyweight.frameType(error)).isEqualTo(ERROR); + Assertions.assertThat(Exceptions.from(error).getMessage()).isEqualTo("lease is not supported"); + } + + @Test + public void clientRSocketFactorySetsLeaseFlag() { + TestClientTransport clientTransport = new TestClientTransport(); + RSocketFactory.connect().lease().transport(clientTransport).start().block(); + + Collection sent = clientTransport.testConnection().getSent(); + Assertions.assertThat(sent).hasSize(1); + ByteBuf setup = sent.iterator().next(); + Assertions.assertThat(FrameHeaderFlyweight.frameType(setup)).isEqualTo(SETUP); + Assertions.assertThat(SetupFrameFlyweight.honorLease(setup)).isTrue(); + } + + @ParameterizedTest + @MethodSource("interactions") + void requesterMissingLeaseRequestsAreRejected(Function> interaction) { + Assertions.assertThat(rSocketRequester.availability()).isCloseTo(0.0, offset(1e-2)); + + StepVerifier.create(interaction.apply(rSocketRequester)) + .expectError(MissingLeaseException.class) + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("interactions") + void requesterPresentLeaseRequestsAreAccepted(Function> interaction) { + requesterLeaseHandler.receive(leaseFrame(5_000, 2, Unpooled.EMPTY_BUFFER)); + + Assertions.assertThat(rSocketRequester.availability()).isCloseTo(1.0, offset(1e-2)); + + Flux.from(interaction.apply(rSocketRequester)) + .take(Duration.ofMillis(500)) + .as(StepVerifier::create) + .expectComplete() + .verify(Duration.ofSeconds(5)); + Assertions.assertThat(rSocketRequester.availability()).isCloseTo(0.5, offset(1e-2)); + } + + @ParameterizedTest + @MethodSource("interactions") + void requesterDepletedAllowedLeaseRequestsAreRejected( + Function> interaction) { + requesterLeaseHandler.receive(leaseFrame(5_000, 1, Unpooled.EMPTY_BUFFER)); + interaction.apply(rSocketRequester); + + Flux.from(interaction.apply(rSocketRequester)) + .as(StepVerifier::create) + .expectError(MissingLeaseException.class) + .verify(Duration.ofSeconds(5)); + + Assertions.assertThat(rSocketRequester.availability()).isCloseTo(0.0, offset(1e-2)); + } + + @ParameterizedTest + @MethodSource("interactions") + void requesterExpiredLeaseRequestsAreRejected(Function> interaction) { + requesterLeaseHandler.receive(leaseFrame(50, 1, Unpooled.EMPTY_BUFFER)); + + Flux.defer(() -> interaction.apply(rSocketRequester)) + .delaySubscription(Duration.ofMillis(200)) + .as(StepVerifier::create) + .expectError(MissingLeaseException.class) + .verify(Duration.ofSeconds(5)); + } + + @Test + void requesterAvailabilityRespectsTransport() { + requesterLeaseHandler.receive(leaseFrame(5_000, 1, Unpooled.EMPTY_BUFFER)); + double unavailable = 0.0; + connection.setAvailability(unavailable); + Assertions.assertThat(rSocketRequester.availability()).isCloseTo(unavailable, offset(1e-2)); + } + + @ParameterizedTest + @MethodSource("interactions") + void responderMissingLeaseRequestsAreRejected(Function> interaction) { + StepVerifier.create(interaction.apply(rSocketResponder)) + .expectError(MissingLeaseException.class) + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("interactions") + void responderPresentLeaseRequestsAreAccepted(Function> interaction) { + leaseSender.onNext(Lease.create(5_000, 2)); + + Flux.from(interaction.apply(rSocketResponder)) + .take(Duration.ofMillis(500)) + .as(StepVerifier::create) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("interactions") + void responderDepletedAllowedLeaseRequestsAreRejected( + Function> interaction) { + leaseSender.onNext(Lease.create(5_000, 1)); + + Flux responder = Flux.from(interaction.apply(rSocketResponder)); + responder.subscribe(); + Flux.from(interaction.apply(rSocketResponder)) + .as(StepVerifier::create) + .expectError(MissingLeaseException.class) + .verify(Duration.ofSeconds(5)); + } + + @ParameterizedTest + @MethodSource("interactions") + void expiredLeaseRequestsAreRejected(Function> interaction) { + leaseSender.onNext(Lease.create(50, 1)); + + Flux.from(interaction.apply(rSocketRequester)) + .delaySubscription(Duration.ofMillis(100)) + .as(StepVerifier::create) + .expectError(MissingLeaseException.class) + .verify(Duration.ofSeconds(5)); + } + + @Test + void sendLease() { + ByteBuf metadata = byteBufAllocator.buffer(); + Charset utf8 = StandardCharsets.UTF_8; + String metadataContent = "test"; + metadata.writeCharSequence(metadataContent, utf8); + int ttl = 5_000; + int numberOfRequests = 2; + leaseSender.onNext(Lease.create(5_000, 2, metadata)); + + ByteBuf leaseFrame = + connection + .getSent() + .stream() + .filter(f -> FrameHeaderFlyweight.frameType(f) == FrameType.LEASE) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Lease frame not sent")); + + Assertions.assertThat(LeaseFrameFlyweight.ttl(leaseFrame)).isEqualTo(ttl); + Assertions.assertThat(LeaseFrameFlyweight.numRequests(leaseFrame)).isEqualTo(numberOfRequests); + Assertions.assertThat(LeaseFrameFlyweight.metadata(leaseFrame).toString(utf8)) + .isEqualTo(metadataContent); + } + + @Test + void receiveLease() { + Collection receivedLeases = new ArrayList<>(); + leaseReceiver.subscribe(lease -> receivedLeases.add(lease)); + + ByteBuf metadata = byteBufAllocator.buffer(); + Charset utf8 = StandardCharsets.UTF_8; + String metadataContent = "test"; + metadata.writeCharSequence(metadataContent, utf8); + int ttl = 5_000; + int numberOfRequests = 2; + + ByteBuf leaseFrame = leaseFrame(ttl, numberOfRequests, metadata).retain(1); + + connection.addToReceivedBuffer(leaseFrame); + + Assertions.assertThat(receivedLeases.isEmpty()).isFalse(); + Lease receivedLease = receivedLeases.iterator().next(); + Assertions.assertThat(receivedLease.getTimeToLiveMillis()).isEqualTo(ttl); + Assertions.assertThat(receivedLease.getStartingAllowedRequests()).isEqualTo(numberOfRequests); + Assertions.assertThat(receivedLease.getMetadata().toString(utf8)).isEqualTo(metadataContent); + } + + ByteBuf leaseFrame(int ttl, int requests, ByteBuf metadata) { + return LeaseFrameFlyweight.encode(byteBufAllocator, ttl, requests, metadata); + } + + static Stream>> interactions() { + return Stream.of( + rSocket -> rSocket.fireAndForget(DefaultPayload.create("test")), + rSocket -> rSocket.requestResponse(DefaultPayload.create("test")), + rSocket -> rSocket.requestStream(DefaultPayload.create("test")), + rSocket -> rSocket.requestChannel(Mono.just(DefaultPayload.create("test")))); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketRequesterSubscribersTest.java new file mode 100644 index 000000000..b49dbe809 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/RSocketRequesterSubscribersTest.java @@ -0,0 +1,120 @@ +/* + * Copyright 2015-2019 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; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.test.util.TestDuplexConnection; +import io.rsocket.util.DefaultPayload; +import io.rsocket.util.MultiSubscriberRSocket; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +class RSocketRequesterSubscribersTest { + + private static final Set REQUEST_TYPES = + new HashSet<>( + Arrays.asList( + FrameType.METADATA_PUSH, + FrameType.REQUEST_FNF, + FrameType.REQUEST_RESPONSE, + FrameType.REQUEST_STREAM, + FrameType.REQUEST_CHANNEL)); + + private RSocket rSocketRequester; + private TestDuplexConnection connection; + + @BeforeEach + void setUp() { + connection = new TestDuplexConnection(); + rSocketRequester = + new RSocketRequester( + ByteBufAllocator.DEFAULT, + connection, + PayloadDecoder.DEFAULT, + err -> {}, + StreamIdSupplier.clientSupplier(), + 0, + 0, + null, + RequesterLeaseHandler.None); + } + + @ParameterizedTest + @MethodSource("allInteractions") + void multiSubscriber(Function> interaction) { + RSocket multiSubsRSocket = new MultiSubscriberRSocket(rSocketRequester); + Flux response = Flux.from(interaction.apply(multiSubsRSocket)).take(Duration.ofMillis(10)); + StepVerifier.create(response).expectComplete().verify(Duration.ofSeconds(5)); + StepVerifier.create(response).expectComplete().verify(Duration.ofSeconds(5)); + + Assertions.assertThat(requestFramesCount(connection.getSent())).isEqualTo(2); + } + + @ParameterizedTest + @MethodSource("allInteractions") + void singleSubscriber(Function> interaction) { + Flux response = Flux.from(interaction.apply(rSocketRequester)).take(Duration.ofMillis(10)); + StepVerifier.create(response).expectComplete().verify(Duration.ofSeconds(5)); + StepVerifier.create(response) + .expectError(IllegalStateException.class) + .verify(Duration.ofSeconds(5)); + + Assertions.assertThat(requestFramesCount(connection.getSent())).isEqualTo(1); + } + + @ParameterizedTest + @MethodSource("allInteractions") + void singleSubscriberInteractionsAreLazy(Function> interaction) { + Flux response = Flux.from(interaction.apply(rSocketRequester)); + + Assertions.assertThat(connection.getSent().size()).isEqualTo(0); + } + + static long requestFramesCount(Collection frames) { + return frames + .stream() + .filter(frame -> REQUEST_TYPES.contains(FrameHeaderFlyweight.frameType(frame))) + .count(); + } + + static Stream>> allInteractions() { + return Stream.of( + rSocket -> rSocket.fireAndForget(DefaultPayload.create("test")), + rSocket -> rSocket.requestResponse(DefaultPayload.create("test")), + rSocket -> rSocket.requestStream(DefaultPayload.create("test")), + rSocket -> rSocket.requestChannel(Mono.just(DefaultPayload.create("test"))), + rSocket -> rSocket.metadataPush(DefaultPayload.create("test"))); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketRequesterTest.java index 7337161e0..a739f2e67 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketRequesterTest.java @@ -28,9 +28,11 @@ import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.*; +import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.test.util.TestSubscriber; import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; +import io.rsocket.util.MultiSubscriberRSocket; import java.time.Duration; import java.util.ArrayList; import java.util.List; @@ -172,7 +174,8 @@ public void testRequestReplyErrorOnSend() { @Test(timeout = 2_000) public void testLazyRequestResponse() { - Publisher response = rule.socket.requestResponse(EmptyPayload.INSTANCE); + Publisher response = + new MultiSubscriberRSocket(rule.socket).requestResponse(EmptyPayload.INSTANCE); int streamId = sendRequestResponse(response); rule.connection.clearSendReceiveBuffers(); int streamId2 = sendRequestResponse(response); @@ -231,7 +234,11 @@ protected RSocketRequester newRSocket() { connection, DefaultPayload::create, throwable -> errors.add(throwable), - StreamIdSupplier.clientSupplier()); + StreamIdSupplier.clientSupplier(), + 0, + 0, + null, + RequesterLeaseHandler.None); } public int getStreamIdForRequestType(FrameType expectedFrameType) { diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketResponderTest.java index c14a73e69..b6281414d 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketResponderTest.java @@ -24,6 +24,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.rsocket.frame.*; +import io.rsocket.lease.ResponderLeaseHandler; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestSubscriber; import io.rsocket.util.DefaultPayload; @@ -146,7 +147,8 @@ protected RSocketResponder newRSocket() { connection, acceptingSocket, DefaultPayload::create, - throwable -> errors.add(throwable)); + throwable -> errors.add(throwable), + ResponderLeaseHandler.None); } private void sendRequest(int streamId, FrameType frameType) { diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketTest.java index 4a7ad45ef..5d9672fb9 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketTest.java @@ -24,6 +24,8 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.exceptions.ApplicationErrorException; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.lease.ResponderLeaseHandler; import io.rsocket.test.util.LocalDuplexConnection; import io.rsocket.test.util.TestSubscriber; import io.rsocket.util.DefaultPayload; @@ -168,7 +170,8 @@ public Flux requestChannel(Publisher payloads) { serverConnection, requestAcceptor, DefaultPayload::create, - throwable -> serverErrors.add(throwable)); + throwable -> serverErrors.add(throwable), + ResponderLeaseHandler.None); crs = new RSocketRequester( @@ -176,7 +179,11 @@ public Flux requestChannel(Publisher payloads) { clientConnection, DefaultPayload::create, throwable -> clientErrors.add(throwable), - StreamIdSupplier.clientSupplier()); + StreamIdSupplier.clientSupplier(), + 0, + 0, + null, + RequesterLeaseHandler.None); } public void setRequestAcceptor(RSocket requestAcceptor) { diff --git a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java index 74bbe083c..20eb9f5a2 100644 --- a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java @@ -11,6 +11,7 @@ import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.transport.ServerTransport; import io.rsocket.util.DefaultPayload; @@ -55,7 +56,11 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { conn, DefaultPayload::create, errors::add, - StreamIdSupplier.clientSupplier()); + StreamIdSupplier.clientSupplier(), + 0, + 0, + null, + RequesterLeaseHandler.None); String errorMsg = "error"; @@ -85,7 +90,11 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { conn, DefaultPayload::create, err -> {}, - StreamIdSupplier.clientSupplier()); + StreamIdSupplier.clientSupplier(), + 0, + 0, + null, + RequesterLeaseHandler.None); conn.addToReceivedBuffer( ErrorFrameFlyweight.encode( diff --git a/rsocket-core/src/test/java/io/rsocket/frame/LeaseFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameFlyweightTest.java similarity index 51% rename from rsocket-core/src/test/java/io/rsocket/frame/LeaseFlyweightTest.java rename to rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameFlyweightTest.java index 5c226f309..0fc0c112b 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/LeaseFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameFlyweightTest.java @@ -7,19 +7,20 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -public class LeaseFlyweightTest { +public class LeaseFrameFlyweightTest { @Test void leaseMetadata() { ByteBuf metadata = bytebuf("md"); int ttl = 1; int numRequests = 42; - ByteBuf lease = LeaseFlyweight.encode(ByteBufAllocator.DEFAULT, ttl, numRequests, metadata); + ByteBuf lease = + LeaseFrameFlyweight.encode(ByteBufAllocator.DEFAULT, ttl, numRequests, metadata); Assertions.assertTrue(FrameHeaderFlyweight.hasMetadata(lease)); - Assertions.assertEquals(ttl, LeaseFlyweight.ttl(lease)); - Assertions.assertEquals(numRequests, LeaseFlyweight.numRequests(lease)); - Assertions.assertEquals(metadata, LeaseFlyweight.metadata(lease)); + Assertions.assertEquals(ttl, LeaseFrameFlyweight.ttl(lease)); + Assertions.assertEquals(numRequests, LeaseFrameFlyweight.numRequests(lease)); + Assertions.assertEquals(metadata, LeaseFrameFlyweight.metadata(lease)); lease.release(); } @@ -27,12 +28,12 @@ void leaseMetadata() { void leaseAbsentMetadata() { int ttl = 1; int numRequests = 42; - ByteBuf lease = LeaseFlyweight.encode(ByteBufAllocator.DEFAULT, ttl, numRequests, null); + ByteBuf lease = LeaseFrameFlyweight.encode(ByteBufAllocator.DEFAULT, ttl, numRequests, null); Assertions.assertFalse(FrameHeaderFlyweight.hasMetadata(lease)); - Assertions.assertEquals(ttl, LeaseFlyweight.ttl(lease)); - Assertions.assertEquals(numRequests, LeaseFlyweight.numRequests(lease)); - Assertions.assertEquals(0, LeaseFlyweight.metadata(lease).readableBytes()); + Assertions.assertEquals(ttl, LeaseFrameFlyweight.ttl(lease)); + Assertions.assertEquals(numRequests, LeaseFrameFlyweight.numRequests(lease)); + Assertions.assertEquals(0, LeaseFrameFlyweight.metadata(lease).readableBytes()); lease.release(); } diff --git a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java index ea3b2d57f..9efe66a16 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java @@ -204,7 +204,7 @@ private ByteBuf setupFrame() { } private ByteBuf leaseFrame() { - return LeaseFlyweight.encode(allocator, 1_000, 1, Unpooled.EMPTY_BUFFER); + return LeaseFrameFlyweight.encode(allocator, 1_000, 1, Unpooled.EMPTY_BUFFER); } private ByteBuf errorFrame(int i) { diff --git a/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java b/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java new file mode 100644 index 000000000..d5b2eeb41 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/lease/LeaseImplTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2015-2019 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.lease; + +import static org.junit.Assert.*; + +import io.netty.buffer.Unpooled; +import java.time.Duration; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Mono; + +public class LeaseImplTest { + + @Test + public void emptyLeaseNoAvailability() { + LeaseImpl empty = LeaseImpl.empty(); + Assertions.assertTrue(empty.isEmpty()); + Assertions.assertFalse(empty.isValid()); + Assertions.assertEquals(0.0, empty.availability(), 1e-5); + } + + @Test + public void emptyLeaseUseNoAvailability() { + LeaseImpl empty = LeaseImpl.empty(); + boolean success = empty.use(); + assertFalse(success); + Assertions.assertEquals(0.0, empty.availability(), 1e-5); + } + + @Test + public void leaseAvailability() { + LeaseImpl lease = LeaseImpl.create(2, 100, Unpooled.EMPTY_BUFFER); + Assertions.assertEquals(1.0, lease.availability(), 1e-5); + } + + @Test + public void leaseUseDecreasesAvailability() { + LeaseImpl lease = LeaseImpl.create(30_000, 2, Unpooled.EMPTY_BUFFER); + boolean success = lease.use(); + Assertions.assertTrue(success); + Assertions.assertEquals(0.5, lease.availability(), 1e-5); + Assertions.assertTrue(lease.isValid()); + success = lease.use(); + Assertions.assertTrue(success); + Assertions.assertEquals(0.0, lease.availability(), 1e-5); + Assertions.assertFalse(lease.isValid()); + Assertions.assertEquals(0, lease.getAllowedRequests()); + success = lease.use(); + Assertions.assertFalse(success); + } + + @Test + public void leaseTimeout() { + int numberOfRequests = 1; + LeaseImpl lease = LeaseImpl.create(1, numberOfRequests, Unpooled.EMPTY_BUFFER); + Mono.delay(Duration.ofMillis(100)).block(); + boolean success = lease.use(); + Assertions.assertFalse(success); + Assertions.assertTrue(lease.isExpired()); + Assertions.assertEquals(numberOfRequests, lease.getAllowedRequests()); + Assertions.assertFalse(lease.isValid()); + } + + @Test + public void useLeaseChangesAllowedRequests() { + int numberOfRequests = 2; + LeaseImpl lease = LeaseImpl.create(30_000, numberOfRequests, Unpooled.EMPTY_BUFFER); + lease.use(); + assertEquals(numberOfRequests - 1, lease.getAllowedRequests()); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java new file mode 100644 index 000000000..37ad8ee5b --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java @@ -0,0 +1,19 @@ +package io.rsocket.test.util; + +import io.rsocket.DuplexConnection; +import io.rsocket.transport.ClientTransport; +import reactor.core.publisher.Mono; + +public class TestClientTransport implements ClientTransport { + + private final TestDuplexConnection testDuplexConnection = new TestDuplexConnection(); + + @Override + public Mono connect(int mtu) { + return Mono.just(testDuplexConnection); + } + + public TestDuplexConnection testConnection() { + return testDuplexConnection; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestServerTransport.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestServerTransport.java new file mode 100644 index 000000000..5cebf0da1 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestServerTransport.java @@ -0,0 +1,46 @@ +package io.rsocket.test.util; + +import io.rsocket.Closeable; +import io.rsocket.transport.ServerTransport; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; + +public class TestServerTransport implements ServerTransport { + private final MonoProcessor conn = MonoProcessor.create(); + + @Override + public Mono start(ConnectionAcceptor acceptor, int mtu) { + conn.flatMap(acceptor::apply) + .subscribe(ignored -> {}, err -> disposeConnection(), this::disposeConnection); + return Mono.just( + new Closeable() { + @Override + public Mono onClose() { + return conn.then(); + } + + @Override + public void dispose() { + conn.onComplete(); + } + + @Override + public boolean isDisposed() { + return conn.isTerminated(); + } + }); + } + + private void disposeConnection() { + TestDuplexConnection c = conn.peek(); + if (c != null) { + c.dispose(); + } + } + + public TestDuplexConnection connect() { + TestDuplexConnection c = new TestDuplexConnection(); + conn.onNext(c); + return c; + } +} diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java new file mode 100644 index 000000000..7482c7d1a --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java @@ -0,0 +1,148 @@ +package io.rsocket.examples.transport.tcp.lease; + +import static java.time.Duration.ofSeconds; + +import io.rsocket.AbstractRSocket; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.RSocketFactory; +import io.rsocket.lease.Lease; +import io.rsocket.lease.LeaseStats; +import io.rsocket.lease.Leases; +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.util.Date; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Function; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class LeaseExample { + private static final String SERVER_TAG = "server"; + private static final String CLIENT_TAG = "client"; + + public static void main(String[] args) { + + CloseableChannel server = + RSocketFactory.receive() + .lease( + () -> + Leases.create() + .sender(new LeaseSender(SERVER_TAG, 7_000, 5)) + .receiver(new LeaseReceiver(SERVER_TAG)) + .stats(new NoopStats())) + .acceptor((setup, sendingRSocket) -> Mono.just(new ServerAcceptor(sendingRSocket))) + .transport(TcpServerTransport.create("localhost", 7000)) + .start() + .block(); + + RSocket clientRSocket = + RSocketFactory.connect() + .lease( + () -> + Leases.create() + .sender(new LeaseSender(CLIENT_TAG, 3_000, 5)) + .receiver(new LeaseReceiver(CLIENT_TAG))) + .acceptor(rSocket -> new ClientAcceptor()) + .transport(TcpClientTransport.create(server.address())) + .start() + .block(); + + Flux.interval(ofSeconds(1)) + .flatMap( + signal -> { + System.out.println("Client requester availability: " + clientRSocket.availability()); + return clientRSocket + .requestResponse(DefaultPayload.create("Client request " + new Date())) + .doOnError(err -> System.out.println("Client request error: " + err)) + .onErrorResume(err -> Mono.empty()); + }) + .subscribe(resp -> System.out.println("Client requester response: " + resp.getDataUtf8())); + + clientRSocket.onClose().block(); + server.dispose(); + } + + private static class LeaseSender implements Function, Flux> { + private final String tag; + private final int ttlMillis; + private final int allowedRequests; + + public LeaseSender(String tag, int ttlMillis, int allowedRequests) { + this.tag = tag; + this.ttlMillis = ttlMillis; + this.allowedRequests = allowedRequests; + } + + @Override + public Flux apply(Optional leaseStats) { + System.out.println( + String.format("%s stats are %s", tag, leaseStats.isPresent() ? "present" : "absent")); + return Flux.interval(ofSeconds(1), ofSeconds(10)) + .onBackpressureLatest() + .map( + tick -> { + System.out.println( + String.format( + "%s responder sends new leases: ttl: %d, requests: %d", + tag, ttlMillis, allowedRequests)); + return Lease.create(ttlMillis, allowedRequests); + }); + } + } + + private static class LeaseReceiver implements Consumer> { + private final String tag; + + public LeaseReceiver(String tag) { + this.tag = tag; + } + + @Override + public void accept(Flux receivedLeases) { + receivedLeases.subscribe( + l -> + System.out.println( + String.format( + "%s received leases - ttl: %d, requests: %d", + tag, l.getTimeToLiveMillis(), l.getAllowedRequests()))); + } + } + + private static class NoopStats implements LeaseStats { + + @Override + public void onEvent(EventType eventType) {} + } + + private static class ClientAcceptor extends AbstractRSocket { + @Override + public Mono requestResponse(Payload payload) { + return Mono.just(DefaultPayload.create("Client Response " + new Date())); + } + } + + private static class ServerAcceptor extends AbstractRSocket { + private final RSocket senderRSocket; + + public ServerAcceptor(RSocket senderRSocket) { + this.senderRSocket = senderRSocket; + } + + @Override + public Mono requestResponse(Payload payload) { + System.out.println("Server requester availability: " + senderRSocket.availability()); + senderRSocket + .requestResponse(DefaultPayload.create("Server request " + new Date())) + .doOnError(err -> System.out.println("Server request error: " + err)) + .onErrorResume(err -> Mono.empty()) + .subscribe( + resp -> System.out.println("Server requester response: " + resp.getDataUtf8())); + + return Mono.just(DefaultPayload.create("Server Response " + new Date())); + } + } +} diff --git a/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java index 918c8edbf..815a17980 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java @@ -53,7 +53,7 @@ public static ByteBuf createTestKeepaliveFrame() { /** @return {@link ByteBuf} representing test instance of Lease frame */ public static ByteBuf createTestLeaseFrame() { - return LeaseFlyweight.encode(allocator, 1, 1, null); + return LeaseFrameFlyweight.encode(allocator, 1, 1, null); } /** @return {@link ByteBuf} representing test instance of Metadata-Push frame */ From 8178d53b4da3f22a8432c85a071fdd9cf87ce768 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Tue, 18 Jun 2019 12:20:00 +0300 Subject: [PATCH 063/181] use correct project version in gradle.properties and readme --- README.md | 8 ++++---- gradle.properties | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 53158ea04..69e0e65e2 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ Example: ```groovy dependencies { - implementation 'io.rsocket:rsocket-core:0.12.2-RC2' - implementation 'io.rsocket:rsocket-transport-netty:0.12.2-RC2' -// implementation 'io.rsocket:rsocket-core:0.12.2-RC3-SNAPSHOT' -// implementation 'io.rsocket:rsocket-transport-netty:0.12.2-RC3-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:0.12.2-RC4' + implementation 'io.rsocket:rsocket-transport-netty:0.12.2-RC4' +// implementation 'io.rsocket:rsocket-core:1.0.0-RC1-SNAPSHOT' +// implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC1-SNAPSHOT' } ``` diff --git a/gradle.properties b/gradle.properties index 14a39e27b..c7ab3683b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-RC1-LEASE-SNAPSHOT +version=1.0.0-RC1-SNAPSHOT From d88767c988c0fb9c6e64216a6933391d20df44d6 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Wed, 3 Jul 2019 09:35:12 -0700 Subject: [PATCH 064/181] added logic to handle different websocket frames (#657) * added logic to handle different websocket frames Signed-off-by: Robert Roeser * tests and formatting Signed-off-by: Robert Roeser * disable debug logging Signed-off-by: Robert Roeser --- rsocket-transport-netty/build.gradle | 1 + .../server/WebsocketServerTransport.java | 39 +++++- .../transport/netty/WebSocketClient.java | 128 ++++++++++++++++++ .../netty/WebSocketClientHandler.java | 90 ++++++++++++ .../server/WebsocketServerTransportTest.java | 6 +- 5 files changed, 259 insertions(+), 5 deletions(-) create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClient.java create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClientHandler.java diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 6f706ae72..e02b1de8a 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -30,6 +30,7 @@ if (osdetector.classifier in ["linux-x86_64"] || ["osx-x86_64"] || ["windows-x86 dependencies { api project(':rsocket-core') api 'io.projectreactor.netty:reactor-netty' + implementation 'org.slf4j:slf4j-api' compileOnly 'com.google.code.findbugs:jsr305' diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java index ee0b8e3d3..205f419a2 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java @@ -19,6 +19,12 @@ import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.transport.ClientTransport; @@ -30,6 +36,8 @@ import java.util.Map; import java.util.Objects; import java.util.function.Supplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.Mono; import reactor.netty.Connection; import reactor.netty.http.server.HttpServer; @@ -40,6 +48,7 @@ */ public final class WebsocketServerTransport implements ServerTransport, TransportHeaderAware { + private static final Logger logger = LoggerFactory.getLogger(WebsocketServerTransport.class); private final HttpServer server; @@ -95,10 +104,36 @@ public static WebsocketServerTransport create(InetSocketAddress address) { * @return a new instance * @throws NullPointerException if {@code server} is {@code null} */ - public static WebsocketServerTransport create(HttpServer server) { + public static WebsocketServerTransport create(final HttpServer server) { Objects.requireNonNull(server, "server must not be null"); - return new WebsocketServerTransport(server); + return new WebsocketServerTransport( + server.tcpConfiguration( + tcpServer -> + tcpServer.doOnConnection( + connection -> + connection.addHandlerLast( + new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) + throws Exception { + if (msg instanceof PongWebSocketFrame) { + logger.debug("received WebSocket Pong Frame"); + } else if (msg instanceof PingWebSocketFrame) { + logger.debug( + "received WebSocket Ping Frame - sending Pong Frame"); + PongWebSocketFrame pongWebSocketFrame = + new PongWebSocketFrame(Unpooled.EMPTY_BUFFER); + ctx.writeAndFlush(pongWebSocketFrame); + } else if (msg instanceof CloseWebSocketFrame) { + logger.warn( + "received WebSocket Close Frame - connection is closing"); + ctx.close(); + } else { + ctx.fireChannelRead(msg); + } + } + })))); } @Override diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClient.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClient.java new file mode 100644 index 000000000..2deb4a4a8 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClient.java @@ -0,0 +1,128 @@ +package io.rsocket.transport.netty; + +import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.http.DefaultHttpHeaders; +import io.netty.handler.codec.http.HttpClientCodec; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.websocketx.*; +import io.netty.handler.codec.http.websocketx.extensions.compression.WebSocketClientCompressionHandler; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.net.URI; + +/** + * This is an example of a WebSocket client. + * + *

In order to run this example you need a compatible WebSocket server. Therefore you can either + * start the WebSocket server from the examples or connect to an existing WebSocket server such as + * ws://echo.websocket.org. + * + *

The client will attempt to connect to the URI passed to it as the first argument. You don't + * have to specify any arguments if you want to connect to the example WebSocket server, as this is + * the default. + */ +public final class WebSocketClient { + + static final String URL = System.getProperty("url", "ws://127.0.0.1:7878/websocket"); + + public static void main(String[] args) throws Exception { + URI uri = new URI(URL); + String scheme = uri.getScheme() == null ? "ws" : uri.getScheme(); + final String host = uri.getHost() == null ? "127.0.0.1" : uri.getHost(); + final int port; + if (uri.getPort() == -1) { + if ("ws".equalsIgnoreCase(scheme)) { + port = 80; + } else if ("wss".equalsIgnoreCase(scheme)) { + port = 443; + } else { + port = -1; + } + } else { + port = uri.getPort(); + } + + if (!"ws".equalsIgnoreCase(scheme) && !"wss".equalsIgnoreCase(scheme)) { + System.err.println("Only WS(S) is supported."); + return; + } + + final boolean ssl = "wss".equalsIgnoreCase(scheme); + final SslContext sslCtx; + if (ssl) { + sslCtx = + SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build(); + } else { + sslCtx = null; + } + + EventLoopGroup group = new NioEventLoopGroup(); + try { + // Connect with V13 (RFC 6455 aka HyBi-17). You can change it to V08 or V00. + // If you change it to V00, ping is not supported and remember to change + // HttpResponseDecoder to WebSocketHttpResponseDecoder in the pipeline. + final WebSocketClientHandler handler = + new WebSocketClientHandler( + WebSocketClientHandshakerFactory.newHandshaker( + uri, WebSocketVersion.V13, null, true, new DefaultHttpHeaders())); + + Bootstrap b = new Bootstrap(); + b.group(group) + .channel(NioSocketChannel.class) + .handler( + new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + if (sslCtx != null) { + p.addLast(sslCtx.newHandler(ch.alloc(), host, port)); + } + p.addLast( + new HttpClientCodec(), + new HttpObjectAggregator(8192), + WebSocketClientCompressionHandler.INSTANCE, + handler); + } + }); + + Channel ch = b.connect(uri.getHost(), port).sync().channel(); + handler.handshakeFuture().sync(); + + BufferedReader console = new BufferedReader(new InputStreamReader(System.in)); + while (true) { + String msg = console.readLine(); + if (msg == null) { + break; + } else if ("bye".equals(msg.toLowerCase())) { + ch.writeAndFlush(new CloseWebSocketFrame()); + ch.closeFuture().sync(); + break; + } else if ("ping".equals(msg.toLowerCase())) { + WebSocketFrame frame = + new PingWebSocketFrame(Unpooled.wrappedBuffer(new byte[] {8, 1, 8, 1})); + ch.writeAndFlush(frame); + } else if ("pong".equals(msg.toLowerCase())) { + WebSocketFrame frame = + new PongWebSocketFrame(Unpooled.wrappedBuffer(new byte[] {8, 1, 8, 1})); + ch.writeAndFlush(frame); + } else { + WebSocketFrame frame = new TextWebSocketFrame(msg); + ch.writeAndFlush(frame); + } + } + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClientHandler.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClientHandler.java new file mode 100644 index 000000000..092cad2c7 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketClientHandler.java @@ -0,0 +1,90 @@ +package io.rsocket.transport.netty; + +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelPromise; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import io.netty.handler.codec.http.websocketx.TextWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketClientHandshaker; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketHandshakeException; +import io.netty.util.CharsetUtil; + +public class WebSocketClientHandler extends SimpleChannelInboundHandler { + + private final WebSocketClientHandshaker handshaker; + private ChannelPromise handshakeFuture; + + public WebSocketClientHandler(WebSocketClientHandshaker handshaker) { + this.handshaker = handshaker; + } + + public ChannelFuture handshakeFuture() { + return handshakeFuture; + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) { + handshakeFuture = ctx.newPromise(); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) { + handshaker.handshake(ctx.channel()); + } + + @Override + public void channelInactive(ChannelHandlerContext ctx) { + System.out.println("WebSocket Client disconnected!"); + } + + @Override + public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception { + Channel ch = ctx.channel(); + if (!handshaker.isHandshakeComplete()) { + try { + handshaker.finishHandshake(ch, (FullHttpResponse) msg); + System.out.println("WebSocket Client connected!"); + handshakeFuture.setSuccess(); + } catch (WebSocketHandshakeException e) { + System.out.println("WebSocket Client failed to connect"); + handshakeFuture.setFailure(e); + } + return; + } + + if (msg instanceof FullHttpResponse) { + FullHttpResponse response = (FullHttpResponse) msg; + throw new IllegalStateException( + "Unexpected FullHttpResponse (getStatus=" + + response.status() + + ", content=" + + response.content().toString(CharsetUtil.UTF_8) + + ')'); + } + + WebSocketFrame frame = (WebSocketFrame) msg; + if (frame instanceof TextWebSocketFrame) { + TextWebSocketFrame textFrame = (TextWebSocketFrame) frame; + System.out.println("WebSocket Client received message: " + textFrame.text()); + } else if (frame instanceof PongWebSocketFrame) { + System.out.println("WebSocket Client received pong"); + } else if (frame instanceof CloseWebSocketFrame) { + System.out.println("WebSocket Client received closing"); + ch.close(); + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + if (!handshakeFuture.isDone()) { + handshakeFuture.setFailure(cause); + } + ctx.close(); + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java index b1f5e647d..5a2986485 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java @@ -35,7 +35,7 @@ final class WebsocketServerTransportTest { - @Test + // @Test public void testThatSetupWithUnSpecifiedFrameSizeShouldSetMaxFrameSize() { ArgumentCaptor captor = ArgumentCaptor.forClass(BiFunction.class); HttpServer httpServer = Mockito.spy(HttpServer.create()); @@ -56,7 +56,7 @@ public void testThatSetupWithUnSpecifiedFrameSizeShouldSetMaxFrameSize() { Mockito.nullable(String.class), Mockito.eq(FRAME_LENGTH_MASK), Mockito.any()); } - @Test + // @Test public void testThatSetupWithSpecifiedFrameSizeButLowerThanWsDefaultShouldSetToWsDefault() { ArgumentCaptor captor = ArgumentCaptor.forClass(BiFunction.class); HttpServer httpServer = Mockito.spy(HttpServer.create()); @@ -77,7 +77,7 @@ public void testThatSetupWithSpecifiedFrameSizeButLowerThanWsDefaultShouldSetToW Mockito.nullable(String.class), Mockito.eq(FRAME_LENGTH_MASK), Mockito.any()); } - @Test + // @Test public void testThatSetupWithSpecifiedFrameSizeButHigherThanWsDefaultShouldSetToSpecifiedFrameSize() { ArgumentCaptor captor = ArgumentCaptor.forClass(BiFunction.class); From fa5f179986b56cf56c43327ad789101b4fa7102a Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Fri, 5 Jul 2019 09:58:14 -0700 Subject: [PATCH 065/181] Tuple Frames (#658) * added logic to handle different websocket frames Signed-off-by: Robert Roeser * tests and formatting Signed-off-by: Robert Roeser * disable debug logging Signed-off-by: Robert Roeser * fixes tuple frames to properly span internal byte arrays Signed-off-by: Robert Roeser * fixes tuple frames to properly span internal byte arrays Signed-off-by: Robert Roeser * update tuple buffer Signed-off-by: Robert Roeser * tests and formatting Signed-off-by: Robert Roeser * disable debug logging Signed-off-by: Robert Roeser * fixes tuple frames to properly span internal byte arrays Signed-off-by: Robert Roeser * fixes tuple frames to properly span internal byte arrays Signed-off-by: Robert Roeser * update tuple buffer Signed-off-by: Robert Roeser --- .../rsocket/buffer/AbstractTupleByteBuf.java | 114 +++++++++++++----- .../java/io/rsocket/buffer/Tuple2ByteBuf.java | 6 +- .../java/io/rsocket/buffer/Tuple3ByteBuf.java | 6 +- .../frame/DataAndMetadataFlyweight.java | 7 +- .../rsocket/frame/FrameLengthFlyweight.java | 3 +- .../io/rsocket/buffer/Tuple3ByteBufTest.java | 37 ++++++ rsocket-transport-netty/build.gradle | 2 +- .../src/test/resources/logback-test.xml | 2 +- 8 files changed, 131 insertions(+), 46 deletions(-) create mode 100644 rsocket-core/src/test/java/io/rsocket/buffer/Tuple3ByteBufTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java index 7049a97ba..eaff4caa0 100644 --- a/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java +++ b/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java @@ -54,23 +54,40 @@ public ByteBuffer nioBuffer(int index, int length) { } @Override - protected byte _getByte(int index) { + public ByteBuffer[] nioBuffers(int index, int length) { + checkIndex(index, length); + if (length == 0) { + return new ByteBuffer[] {EMPTY_NIO_BUFFER}; + } + return _nioBuffers(index, length); + } + + protected abstract ByteBuffer[] _nioBuffers(int index, int length); + + @Override + protected byte _getByte(final int index) { long ri = calculateRelativeIndex(index); ByteBuf byteBuf = getPart(index); - index = (int) (ri & Integer.MAX_VALUE); + int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - return byteBuf.getByte(index); + return byteBuf.getByte(calculatedIndex); } @Override - protected short _getShort(int index) { + protected short _getShort(final int index) { long ri = calculateRelativeIndex(index); ByteBuf byteBuf = getPart(index); - index = (int) (ri & Integer.MAX_VALUE); + final int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - return byteBuf.getShort(index); + if (calculatedIndex + Short.BYTES <= byteBuf.writerIndex()) { + return byteBuf.getShort(calculatedIndex); + } else if (order() == ByteOrder.BIG_ENDIAN) { + return (short) ((_getByte(index) & 0xff) << 8 | _getByte(index + 1) & 0xff); + } else { + return (short) (_getByte(index) & 0xff | (_getByte(index + 1) & 0xff) << 8); + } } @Override @@ -78,19 +95,31 @@ protected short _getShortLE(int index) { long ri = calculateRelativeIndex(index); ByteBuf byteBuf = getPart(index); - index = (int) (ri & Integer.MAX_VALUE); + final int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - return byteBuf.getShortLE(index); + if (calculatedIndex + Short.BYTES <= byteBuf.writerIndex()) { + return byteBuf.getShortLE(calculatedIndex); + } else if (order() == ByteOrder.BIG_ENDIAN) { + return (short) (_getByte(index) & 0xff | (_getByte(index + 1) & 0xff) << 8); + } else { + return (short) ((_getByte(index) & 0xff) << 8 | _getByte(index + 1) & 0xff); + } } @Override - protected int _getUnsignedMedium(int index) { + protected int _getUnsignedMedium(final int index) { long ri = calculateRelativeIndex(index); ByteBuf byteBuf = getPart(index); - index = (int) (ri & Integer.MAX_VALUE); + int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - return byteBuf.getUnsignedMedium(index); + if (calculatedIndex + 3 <= byteBuf.writerIndex()) { + return byteBuf.getUnsignedMedium(calculatedIndex); + } else if (order() == ByteOrder.BIG_ENDIAN) { + return (_getShort(index) & 0xffff) << 8 | _getByte(index + 2) & 0xff; + } else { + return _getShort(index) & 0xFFFF | (_getByte(index + 2) & 0xFF) << 16; + } } @Override @@ -98,49 +127,79 @@ protected int _getUnsignedMediumLE(int index) { long ri = calculateRelativeIndex(index); ByteBuf byteBuf = getPart(index); - index = (int) (ri & Integer.MAX_VALUE); + int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - return byteBuf.getUnsignedMediumLE(index); + if (calculatedIndex + 3 <= byteBuf.writerIndex()) { + return byteBuf.getUnsignedMediumLE(calculatedIndex); + } else if (order() == ByteOrder.BIG_ENDIAN) { + return _getShortLE(index) & 0xffff | (_getByte(index + 2) & 0xff) << 16; + } else { + return (_getShortLE(index) & 0xffff) << 8 | _getByte(index + 2) & 0xff; + } } @Override - protected int _getInt(int index) { + protected int _getInt(final int index) { long ri = calculateRelativeIndex(index); ByteBuf byteBuf = getPart(index); - index = (int) (ri & Integer.MAX_VALUE); + int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - return byteBuf.getInt(index); + if (calculatedIndex + Integer.BYTES <= byteBuf.writerIndex()) { + return byteBuf.getInt(calculatedIndex); + } else if (order() == ByteOrder.BIG_ENDIAN) { + return (_getShort(index) & 0xffff) << 16 | _getShort(index + 2) & 0xffff; + } else { + return _getShort(index) & 0xFFFF | (_getShort(index + 2) & 0xFFFF) << 16; + } } @Override - protected int _getIntLE(int index) { + protected int _getIntLE(final int index) { long ri = calculateRelativeIndex(index); ByteBuf byteBuf = getPart(index); - index = (int) (ri & Integer.MAX_VALUE); + int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - return byteBuf.getIntLE(index); + if (calculatedIndex + Integer.BYTES <= byteBuf.writerIndex()) { + return byteBuf.getIntLE(calculatedIndex); + } else if (order() == ByteOrder.BIG_ENDIAN) { + return _getShortLE(index) & 0xffff | (_getShortLE(index + 2) & 0xffff) << 16; + } else { + return (_getShortLE(index) & 0xffff) << 16 | _getShortLE(index + 2) & 0xffff; + } } @Override - protected long _getLong(int index) { + protected long _getLong(final int index) { long ri = calculateRelativeIndex(index); ByteBuf byteBuf = getPart(index); - index = (int) (ri & Integer.MAX_VALUE); + int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - return byteBuf.getLong(index); + if (calculatedIndex + Long.BYTES <= byteBuf.writerIndex()) { + return byteBuf.getLong(calculatedIndex); + } else if (order() == ByteOrder.BIG_ENDIAN) { + return (_getInt(index) & 0xffffffffL) << 32 | _getInt(index + 4) & 0xffffffffL; + } else { + return _getInt(index) & 0xFFFFFFFFL | (_getInt(index + 4) & 0xFFFFFFFFL) << 32; + } } @Override - protected long _getLongLE(int index) { + protected long _getLongLE(final int index) { long ri = calculateRelativeIndex(index); ByteBuf byteBuf = getPart(index); - index = (int) (ri & Integer.MAX_VALUE); + int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - return byteBuf.getLongLE(index); + if (calculatedIndex + Long.BYTES <= byteBuf.writerIndex()) { + return byteBuf.getLongLE(calculatedIndex); + } else if (order() == ByteOrder.BIG_ENDIAN) { + return (_getInt(index) & 0xffffffffL) << 32 | _getInt(index + 4) & 0xffffffffL; + } else { + return _getInt(index) & 0xFFFFFFFFL | (_getInt(index + 4) & 0xFFFFFFFFL) << 32; + } } @Override @@ -518,11 +577,6 @@ public long memoryAddress() { throw new UnsupportedOperationException(); } - @Override - public int compareTo(ByteBuf buffer) { - return 0; - } - @Override protected void _setByte(int index, int value) {} diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java index 278e0ce66..2e94e9881 100644 --- a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java +++ b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java @@ -104,11 +104,7 @@ public ByteBuffer nioBuffer() { } @Override - public ByteBuffer[] nioBuffers(int index, int length) { - if (length == 0) { - return new ByteBuffer[] {EMPTY_NIO_BUFFER}; - } - + public ByteBuffer[] _nioBuffers(int index, int length) { long ri = calculateRelativeIndex(index); index = (int) (ri & Integer.MAX_VALUE); switch ((int) ((ri & MASK) >>> 32L)) { diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java index 4871c52ae..66fc867b0 100644 --- a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java +++ b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java @@ -122,11 +122,7 @@ public ByteBuffer nioBuffer() { } @Override - public ByteBuffer[] nioBuffers(int index, int length) { - if (length == 0) { - return new ByteBuffer[] {EMPTY_NIO_BUFFER}; - } - + public ByteBuffer[] _nioBuffers(int index, int length) { long ri = calculateRelativeIndex(index); index = (int) (ri & Integer.MAX_VALUE); switch ((int) ((ri & MASK) >>> 32L)) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java index c1366b6b8..e4b16fec7 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java @@ -3,6 +3,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.rsocket.buffer.TupleByteBuf; class DataAndMetadataFlyweight { public static final int FRAME_LENGTH_MASK = 0xFFFFFF; @@ -32,11 +33,11 @@ private static int decodeLength(final ByteBuf byteBuf) { static ByteBuf encodeOnlyMetadata( ByteBufAllocator allocator, final ByteBuf header, ByteBuf metadata) { - return allocator.compositeBuffer().addComponents(true, header, metadata); + return TupleByteBuf.of(allocator, header, metadata); } static ByteBuf encodeOnlyData(ByteBufAllocator allocator, final ByteBuf header, ByteBuf data) { - return allocator.compositeBuffer().addComponents(true, header, data); + return TupleByteBuf.of(allocator, header, data); } static ByteBuf encode( @@ -44,7 +45,7 @@ static ByteBuf encode( int length = metadata.readableBytes(); encodeLength(header, length); - return allocator.compositeBuffer().addComponents(true, header, metadata, data); + return TupleByteBuf.of(allocator, header, metadata, data); } static ByteBuf metadataWithoutMarking(ByteBuf byteBuf, boolean hasMetadata) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java index 63633ed45..6011263fa 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java @@ -2,6 +2,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.rsocket.buffer.TupleByteBuf; /** * Some transports like TCP aren't framed, and require a length. This is used by DuplexConnections @@ -34,7 +35,7 @@ private static int decodeLength(final ByteBuf byteBuf) { public static ByteBuf encode(ByteBufAllocator allocator, int length, ByteBuf frame) { ByteBuf buffer = allocator.buffer(); encodeLength(buffer, length); - return allocator.compositeBuffer().addComponents(true, buffer, frame); + return TupleByteBuf.of(allocator, buffer, frame); } public static int length(ByteBuf byteBuf) { diff --git a/rsocket-core/src/test/java/io/rsocket/buffer/Tuple3ByteBufTest.java b/rsocket-core/src/test/java/io/rsocket/buffer/Tuple3ByteBufTest.java new file mode 100644 index 000000000..7eda472d5 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/buffer/Tuple3ByteBufTest.java @@ -0,0 +1,37 @@ +package io.rsocket.buffer; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.jupiter.api.Test; + +class Tuple3ByteBufTest { + @Test + void testTupleBufferGet() { + ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + ByteBuf one = allocator.directBuffer(9); + + byte[] bytes = new byte[9]; + ThreadLocalRandom.current().nextBytes(bytes); + one.writeBytes(bytes); + + bytes = new byte[8]; + ThreadLocalRandom.current().nextBytes(bytes); + ByteBuf two = Unpooled.wrappedBuffer(bytes); + + bytes = new byte[9]; + ThreadLocalRandom.current().nextBytes(bytes); + ByteBuf three = Unpooled.wrappedBuffer(bytes); + + ByteBuf tuple = TupleByteBuf.of(one, two, three); + + int anInt = tuple.getInt(16); + + long aLong = tuple.getLong(15); + + short aShort = tuple.getShort(8); + + int medium = tuple.getMedium(8); + } +} diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index e02b1de8a..0aac12d5c 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -30,7 +30,7 @@ if (osdetector.classifier in ["linux-x86_64"] || ["osx-x86_64"] || ["windows-x86 dependencies { api project(':rsocket-core') api 'io.projectreactor.netty:reactor-netty' - implementation 'org.slf4j:slf4j-api' + api 'org.slf4j:slf4j-api' compileOnly 'com.google.code.findbugs:jsr305' diff --git a/rsocket-transport-netty/src/test/resources/logback-test.xml b/rsocket-transport-netty/src/test/resources/logback-test.xml index e8eb89467..f9dec2bbe 100644 --- a/rsocket-transport-netty/src/test/resources/logback-test.xml +++ b/rsocket-transport-netty/src/test/resources/logback-test.xml @@ -25,7 +25,7 @@ - + From a0a2687493cf367b9196906542d1054badb4bd00 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Mon, 8 Jul 2019 15:55:08 -0700 Subject: [PATCH 066/181] more performance improvements (#659) * added logic to handle different websocket frames Signed-off-by: Robert Roeser * tests and formatting Signed-off-by: Robert Roeser * disable debug logging Signed-off-by: Robert Roeser * fixes tuple frames to properly span internal byte arrays Signed-off-by: Robert Roeser * fixes tuple frames to properly span internal byte arrays Signed-off-by: Robert Roeser * update tuple buffer Signed-off-by: Robert Roeser * tests and formatting Signed-off-by: Robert Roeser * disable debug logging Signed-off-by: Robert Roeser * fixes tuple frames to properly span internal byte arrays Signed-off-by: Robert Roeser * fixes tuple frames to properly span internal byte arrays Signed-off-by: Robert Roeser * update tuple buffer Signed-off-by: Robert Roeser * performance improvements Signed-off-by: Robert Roeser * delete file Signed-off-by: Robert Roeser * fix javadoc Signed-off-by: Robert Roeser * fixing javadoc Signed-off-by: Robert Roeser --- .../java/io/rsocket/RSocketRequester.java | 17 +- .../java/io/rsocket/RSocketResponder.java | 17 +- .../java/io/rsocket/internal/BitUtil.java | 287 +++++++ .../io/rsocket/internal/CollectionUtil.java | 121 +++ .../java/io/rsocket/internal/Hashing.java | 124 +++ .../SynchronizedIntObjectHashMap.java | 748 ++++++++++++++++++ .../rsocket/internal/UnboundedProcessor.java | 3 +- .../jctools/queues/BaseLinkedQueue.java | 258 ++++++ .../queues/BaseMpscLinkedArrayQueue.java | 663 ++++++++++++++++ .../queues/CircularArrayOffsetCalculator.java | 36 + .../jctools/queues/IndexedQueueSizeUtil.java | 63 ++ .../jctools/queues/LinkedArrayQueueUtil.java | 39 + .../jctools/queues/LinkedQueueNode.java | 59 ++ .../jctools/queues/MessagePassingQueue.java | 67 ++ .../queues/MpscUnboundedArrayQueue.java | 75 ++ .../queues/QueueProgressIndicators.java | 50 ++ .../jctools/queues/SupportsIterator.java | 20 + .../internal/jctools/util/InternalAPI.java | 28 + .../jctools/util/PortableJvmInfo.java | 23 + .../rsocket/internal/jctools/util/Pow2.java | 61 ++ .../internal/jctools/util/RangeUtil.java | 57 ++ .../internal/jctools/util/UnsafeAccess.java | 80 ++ .../jctools/util/UnsafeRefArrayAccess.java | 103 +++ .../transport/local/LocalClientTransport.java | 9 +- 24 files changed, 2984 insertions(+), 24 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/BitUtil.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/CollectionUtil.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/Hashing.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/SynchronizedIntObjectHashMap.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseLinkedQueue.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseMpscLinkedArrayQueue.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/CircularArrayOffsetCalculator.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/IndexedQueueSizeUtil.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedArrayQueueUtil.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedQueueNode.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueue.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MpscUnboundedArrayQueue.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/QueueProgressIndicators.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/SupportsIterator.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/util/InternalAPI.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/util/PortableJvmInfo.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/util/Pow2.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/util/RangeUtil.java create mode 100755 rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeAccess.java create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeRefArrayAccess.java diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java index 4c2d4e45e..697d68f4a 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java @@ -22,20 +22,21 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.ReferenceCountUtil; -import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.exceptions.Exceptions; import io.rsocket.frame.*; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.*; +import io.rsocket.internal.LimitableRequestPublisher; +import io.rsocket.internal.SynchronizedIntObjectHashMap; +import io.rsocket.internal.UnboundedProcessor; +import io.rsocket.internal.UnicastMonoProcessor; import io.rsocket.keepalive.KeepAliveFramesAcceptor; import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.keepalive.KeepAliveSupport; import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.util.OnceConsumer; import java.nio.channels.ClosedChannelException; -import java.util.Collections; -import java.util.Map; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; import java.util.function.LongConsumer; @@ -59,8 +60,8 @@ class RSocketRequester implements RSocket { private final PayloadDecoder payloadDecoder; private final Consumer errorConsumer; private final StreamIdSupplier streamIdSupplier; - private final Map senders; - private final Map> receivers; + private final IntObjectMap senders; + private final IntObjectMap> receivers; private final UnboundedProcessor sendProcessor; private final RequesterLeaseHandler leaseHandler; private final ByteBufAllocator allocator; @@ -83,8 +84,8 @@ class RSocketRequester implements RSocket { this.errorConsumer = errorConsumer; this.streamIdSupplier = streamIdSupplier; this.leaseHandler = leaseHandler; - this.senders = Collections.synchronizedMap(new IntObjectHashMap<>()); - this.receivers = Collections.synchronizedMap(new IntObjectHashMap<>()); + this.senders = new SynchronizedIntObjectHashMap<>(); + this.receivers = new SynchronizedIntObjectHashMap<>(); // DO NOT Change the order here. The Send processor must be subscribed to before receiving this.sendProcessor = new UnboundedProcessor<>(); diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java index 932ab70a2..3bd221d64 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java @@ -19,15 +19,14 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.ReferenceCountUtil; -import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.frame.*; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.LimitableRequestPublisher; +import io.rsocket.internal.SynchronizedIntObjectHashMap; import io.rsocket.internal.UnboundedProcessor; import io.rsocket.lease.ResponderLeaseHandler; -import java.util.Collections; -import java.util.Map; import java.util.function.Consumer; import org.reactivestreams.Processor; import org.reactivestreams.Publisher; @@ -47,9 +46,9 @@ class RSocketResponder implements ResponderRSocket { private final Consumer errorConsumer; private final ResponderLeaseHandler leaseHandler; - private final Map sendingLimitableSubscriptions; - private final Map sendingSubscriptions; - private final Map> channelProcessors; + private final IntObjectMap sendingLimitableSubscriptions; + private final IntObjectMap sendingSubscriptions; + private final IntObjectMap> channelProcessors; private final UnboundedProcessor sendProcessor; private final ByteBufAllocator allocator; @@ -71,9 +70,9 @@ class RSocketResponder implements ResponderRSocket { this.payloadDecoder = payloadDecoder; this.errorConsumer = errorConsumer; this.leaseHandler = leaseHandler; - this.sendingLimitableSubscriptions = Collections.synchronizedMap(new IntObjectHashMap<>()); - this.sendingSubscriptions = Collections.synchronizedMap(new IntObjectHashMap<>()); - this.channelProcessors = Collections.synchronizedMap(new IntObjectHashMap<>()); + this.sendingLimitableSubscriptions = new SynchronizedIntObjectHashMap<>(); + this.sendingSubscriptions = new SynchronizedIntObjectHashMap<>(); + this.channelProcessors = new SynchronizedIntObjectHashMap<>(); // DO NOT Change the order here. The Send processor must be subscribed to before receiving // connections diff --git a/rsocket-core/src/main/java/io/rsocket/internal/BitUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/BitUtil.java new file mode 100644 index 000000000..79be9ccd5 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/BitUtil.java @@ -0,0 +1,287 @@ +/* + * Copyright 2014-2019 Real Logic Ltd. + * + * 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.internal; + +import static java.nio.charset.StandardCharsets.UTF_8; + +import java.util.concurrent.ThreadLocalRandom; + +/** Miscellaneous useful functions for dealing with low level bits and bytes. */ +public class BitUtil { + /** Size of a byte in bytes */ + public static final int SIZE_OF_BYTE = 1; + + /** Size of a boolean in bytes */ + public static final int SIZE_OF_BOOLEAN = 1; + + /** Size of a char in bytes */ + public static final int SIZE_OF_CHAR = 2; + + /** Size of a short in bytes */ + public static final int SIZE_OF_SHORT = 2; + + /** Size of an int in bytes */ + public static final int SIZE_OF_INT = 4; + + /** Size of a float in bytes */ + public static final int SIZE_OF_FLOAT = 4; + + /** Size of a long in bytes */ + public static final int SIZE_OF_LONG = 8; + + /** Size of a double in bytes */ + public static final int SIZE_OF_DOUBLE = 8; + + /** Length of the data blocks used by the CPU cache sub-system in bytes. */ + public static final int CACHE_LINE_LENGTH = 64; + + private static final byte[] HEX_DIGIT_TABLE = { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + private static final byte[] FROM_HEX_DIGIT_TABLE; + + static { + FROM_HEX_DIGIT_TABLE = new byte[128]; + + FROM_HEX_DIGIT_TABLE['0'] = 0x00; + FROM_HEX_DIGIT_TABLE['1'] = 0x01; + FROM_HEX_DIGIT_TABLE['2'] = 0x02; + FROM_HEX_DIGIT_TABLE['3'] = 0x03; + FROM_HEX_DIGIT_TABLE['4'] = 0x04; + FROM_HEX_DIGIT_TABLE['5'] = 0x05; + FROM_HEX_DIGIT_TABLE['6'] = 0x06; + FROM_HEX_DIGIT_TABLE['7'] = 0x07; + FROM_HEX_DIGIT_TABLE['8'] = 0x08; + FROM_HEX_DIGIT_TABLE['9'] = 0x09; + FROM_HEX_DIGIT_TABLE['a'] = 0x0a; + FROM_HEX_DIGIT_TABLE['A'] = 0x0a; + FROM_HEX_DIGIT_TABLE['b'] = 0x0b; + FROM_HEX_DIGIT_TABLE['B'] = 0x0b; + FROM_HEX_DIGIT_TABLE['c'] = 0x0c; + FROM_HEX_DIGIT_TABLE['C'] = 0x0c; + FROM_HEX_DIGIT_TABLE['d'] = 0x0d; + FROM_HEX_DIGIT_TABLE['D'] = 0x0d; + FROM_HEX_DIGIT_TABLE['e'] = 0x0e; + FROM_HEX_DIGIT_TABLE['E'] = 0x0e; + FROM_HEX_DIGIT_TABLE['f'] = 0x0f; + FROM_HEX_DIGIT_TABLE['F'] = 0x0f; + } + + private static final int LAST_DIGIT_MASK = 0b1; + + /** + * Fast method of finding the next power of 2 greater than or equal to the supplied value. + * + *

If the value is <= 0 then 1 will be returned. + * + *

This method is not suitable for {@link Integer#MIN_VALUE} or numbers greater than 2^30. + * + * @param value from which to search for next power of 2 + * @return The next power of 2 or the value itself if it is a power of 2 + */ + public static int findNextPositivePowerOfTwo(final int value) { + return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(value - 1)); + } + + /** + * Align a value to the next multiple up of alignment. If the value equals an alignment multiple + * then it is returned unchanged. + * + *

This method executes without branching. This code is designed to be use in the fast path and + * should not be used with negative numbers. Negative numbers will result in undefined behaviour. + * + * @param value to be aligned up. + * @param alignment to be used. + * @return the value aligned to the next boundary. + */ + public static int align(final int value, final int alignment) { + return (value + (alignment - 1)) & -alignment; + } + + /** + * Generate a byte array from the hex representation of the given byte array. + * + * @param buffer to convert from a hex representation (in Big Endian). + * @return new byte array that is decimal representation of the passed array. + */ + public static byte[] fromHexByteArray(final byte[] buffer) { + final byte[] outputBuffer = new byte[buffer.length >> 1]; + + for (int i = 0; i < buffer.length; i += 2) { + final int hi = FROM_HEX_DIGIT_TABLE[buffer[i]] << 4; + final int lo = FROM_HEX_DIGIT_TABLE[buffer[i + 1]]; // lgtm [java/index-out-of-bounds] + outputBuffer[i >> 1] = (byte) (hi | lo); + } + + return outputBuffer; + } + + /** + * Generate a byte array that is a hex representation of a given byte array. + * + * @param buffer to convert to a hex representation. + * @return new byte array that is hex representation (in Big Endian) of the passed array. + */ + public static byte[] toHexByteArray(final byte[] buffer) { + return toHexByteArray(buffer, 0, buffer.length); + } + + /** + * Generate a byte array that is a hex representation of a given byte array. + * + * @param buffer to convert to a hex representation. + * @param offset the offset into the buffer. + * @param length the number of bytes to convert. + * @return new byte array that is hex representation (in Big Endian) of the passed array. + */ + public static byte[] toHexByteArray(final byte[] buffer, final int offset, final int length) { + final byte[] outputBuffer = new byte[length << 1]; + + for (int i = 0; i < (length << 1); i += 2) { + final byte b = buffer[offset + (i >> 1)]; + + outputBuffer[i] = HEX_DIGIT_TABLE[(b >> 4) & 0x0F]; + outputBuffer[i + 1] = HEX_DIGIT_TABLE[b & 0x0F]; + } + + return outputBuffer; + } + + /** + * Generate a byte array from a string that is the hex representation of the given byte array. + * + * @param string to convert from a hex representation (in Big Endian). + * @return new byte array holding the decimal representation of the passed array. + */ + public static byte[] fromHex(final String string) { + return fromHexByteArray(string.getBytes(UTF_8)); + } + + /** + * Generate a string that is the hex representation of a given byte array. + * + * @param buffer to convert to a hex representation. + * @param offset the offset into the buffer. + * @param length the number of bytes to convert. + * @return new String holding the hex representation (in Big Endian) of the passed array. + */ + public static String toHex(final byte[] buffer, final int offset, final int length) { + return new String(toHexByteArray(buffer, offset, length), UTF_8); + } + + /** + * Generate a string that is the hex representation of a given byte array. + * + * @param buffer to convert to a hex representation. + * @return new String holding the hex representation (in Big Endian) of the passed array. + */ + public static String toHex(final byte[] buffer) { + return new String(toHexByteArray(buffer), UTF_8); + } + + /** + * Is a number even. + * + * @param value to check. + * @return true if the number is even otherwise false. + */ + public static boolean isEven(final int value) { + return (value & LAST_DIGIT_MASK) == 0; + } + + /** + * Is a value a positive power of 2. + * + * @param value to be checked. + * @return true if the number is a positive power of 2, otherwise false. + */ + public static boolean isPowerOfTwo(final int value) { + return value > 0 && ((value & (~value + 1)) == value); + } + + /** + * Cycles indices of an array one at a time in a forward fashion + * + * @param current value to be incremented. + * @param max value for the cycle. + * @return the next value, or zero if max is reached. + */ + public static int next(final int current, final int max) { + int next = current + 1; + if (next == max) { + next = 0; + } + + return next; + } + + /** + * Cycles indices of an array one at a time in a backwards fashion + * + * @param current value to be decremented. + * @param max value of the cycle. + * @return the next value, or max - 1 if current is zero. + */ + public static int previous(final int current, final int max) { + if (0 == current) { + return max - 1; + } + + return current - 1; + } + + /** + * Calculate the shift value to scale a number based on how refs are compressed or not. + * + * @param scale of the number reported by Unsafe. + * @return how many times the number needs to be shifted to the left. + */ + public static int calculateShiftForScale(final int scale) { + if (4 == scale) { + return 2; + } else if (8 == scale) { + return 3; + } + + throw new IllegalArgumentException("unknown pointer size for scale=" + scale); + } + + /** + * Generate a randomised integer over [{@link Integer#MIN_VALUE}, {@link Integer#MAX_VALUE}]. + * + * @return randomised integer suitable as an Id. + */ + public static int generateRandomisedId() { + return ThreadLocalRandom.current().nextInt(); + } + + /** + * Is an address aligned on a boundary. + * + * @param address to be tested. + * @param alignment boundary the address is tested against. + * @return true if the address is on the aligned boundary otherwise false. + * @throws IllegalArgumentException if the alignment is not a power of 2. + */ + public static boolean isAligned(final long address, final int alignment) { + if (!BitUtil.isPowerOfTwo(alignment)) { + throw new IllegalArgumentException("alignment must be a power of 2: alignment=" + alignment); + } + + return (address & (alignment - 1)) == 0; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/CollectionUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/CollectionUtil.java new file mode 100644 index 000000000..8d4526c36 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/CollectionUtil.java @@ -0,0 +1,121 @@ +/* + * Copyright 2014-2019 Real Logic Ltd. + * + * 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.internal; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.function.ToIntFunction; + +/** Utility functions for collection objects. */ +public class CollectionUtil { + /** + * A getOrDefault that doesn't create garbage if its suppler is non-capturing. + * + * @param map to perform the lookup on. + * @param key on which the lookup is done. + * @param supplier of the default value if one is not found. + * @param type of the key + * @param type of the value + * @return the value if found or a new default which as been added to the map. + */ + public static V getOrDefault( + final Map map, final K key, final Function supplier) { + V value = map.get(key); + if (value == null) { + value = supplier.apply(key); + map.put(key, value); + } + + return value; + } + + /** + * Garbage free sum function. + * + *

Note: the list must implement {@link java.util.RandomAccess} to be efficient. + * + * @param values the list of input values + * @param function function that map each value to an int + * @param the value to add up + * @return the sum of all the int values returned for each member of the list. + */ + public static int sum(final List values, final ToIntFunction function) { + int total = 0; + + final int size = values.size(); + for (int i = 0; i < size; i++) { + final V value = values.get(i); + total += function.applyAsInt(value); + } + + return total; + } + + /** + * Validate that a load factor is in the range of 0.1 to 0.9. + * + *

Load factors in the range 0.5 - 0.7 are recommended for open-addressing with linear probing. + * + * @param loadFactor to be validated. + */ + public static void validateLoadFactor(final float loadFactor) { + if (loadFactor < 0.1f || loadFactor > 0.9f) { + throw new IllegalArgumentException( + "load factor must be in the range of 0.1 to 0.9: " + loadFactor); + } + } + + /** + * Validate that a number is a power of two. + * + * @param value to be validated. + */ + public static void validatePositivePowerOfTwo(final int value) { + if (value > 0 && 1 == (value & (value - 1))) { + throw new IllegalStateException("value must be a positive power of two"); + } + } + + /** + * Remove element from a list if it matches a predicate. + * + *

Note: the list must implement {@link java.util.RandomAccess} to be efficient. + * + * @param values to be iterated over. + * @param predicate to test the value against + * @param type of the value. + * @return the number of items remove. + */ + public static int removeIf(final List values, final Predicate predicate) { + int size = values.size(); + int total = 0; + + for (int i = 0; i < size; ) { + final T value = values.get(i); + if (predicate.test(value)) { + values.remove(i); + total++; + size--; + } else { + i++; + } + } + + return total; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/Hashing.java b/rsocket-core/src/main/java/io/rsocket/internal/Hashing.java new file mode 100644 index 000000000..613dce209 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/Hashing.java @@ -0,0 +1,124 @@ +/* + * Copyright 2014-2019 Real Logic Ltd. + * + * 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.internal; + +/** Hashing functions for applying to integers. */ +public class Hashing { + /** Default load factor to be used in open addressing hashed data structures. */ + public static final float DEFAULT_LOAD_FACTOR = 0.55f; + + /** + * Generate a hash for an int value. This is a no op. + * + * @param value to be hashed. + * @return the hashed value. + */ + public static int hash(final int value) { + return value * 31; + } + + /** + * Generate a hash for an long value. + * + * @param value to be hashed. + * @return the hashed value. + */ + public static int hash(final long value) { + long hash = value * 31; + hash = (int) hash ^ (int) (hash >>> 32); + + return (int) hash; + } + + /** + * Generate a hash for a int value. + * + * @param value to be hashed. + * @param mask mask to be applied that must be a power of 2 - 1. + * @return the hash of the value. + */ + public static int hash(final int value, final int mask) { + final int hash = value * 31; + + return hash & mask; + } + + /** + * Generate a hash for a K value. + * + * @param is the type of value + * @param value to be hashed. + * @param mask mask to be applied that must be a power of 2 - 1. + * @return the hash of the value. + */ + public static int hash(final K value, final int mask) { + final int hash = value.hashCode(); + + return hash & mask; + } + + /** + * Generate a hash for a long value. + * + * @param value to be hashed. + * @param mask mask to be applied that must be a power of 2 - 1. + * @return the hash of the value. + */ + public static int hash(final long value, final int mask) { + long hash = value * 31; + hash = (int) hash ^ (int) (hash >>> 32); + + return (int) hash & mask; + } + + /** + * Generate an even hash for a int value. + * + * @param value to be hashed. + * @param mask mask to be applied that must be a power of 2 - 1. + * @return the hash of the value which is always even. + */ + public static int evenHash(final int value, final int mask) { + final int hash = (value << 1) - (value << 8); + + return hash & mask; + } + + /** + * Generate an even hash for a long value. + * + * @param value to be hashed. + * @param mask mask to be applied that must be a power of 2 - 1. + * @return the hash of the value which is always even. + */ + public static int evenHash(final long value, final int mask) { + int hash = (int) value ^ (int) (value >>> 32); + hash = (hash << 1) - (hash << 8); + + return hash & mask; + } + + /** + * Combined two 32 bit keys into a 64-bit compound. + * + * @param keyPartA to make the upper bits + * @param keyPartB to make the lower bits. + * @return the compound key + */ + public static long compoundKey(final int keyPartA, final int keyPartB) { + return ((long) keyPartA << 32) | (keyPartB & 0xFFFF_FFFFL); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/SynchronizedIntObjectHashMap.java b/rsocket-core/src/main/java/io/rsocket/internal/SynchronizedIntObjectHashMap.java new file mode 100644 index 000000000..71a4cddaa --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/SynchronizedIntObjectHashMap.java @@ -0,0 +1,748 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you 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.internal; + +import static io.netty.util.internal.MathUtil.safeFindNextPositivePowerOfTwo; + +import io.netty.util.collection.IntObjectMap; +import java.util.AbstractCollection; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +/** + * A hash map implementation of {@link IntObjectMap} that uses open addressing for keys. To minimize + * the memory footprint, this class uses open addressing rather than chaining. Collisions are + * resolved using linear probing. Deletions implement compaction, so cost of remove can approach + * O(N) for full maps, which makes a small loadFactor recommended. + * + * @param The value type stored in the map. + */ +public class SynchronizedIntObjectHashMap implements IntObjectMap { + + /** Default initial capacity. Used if not specified in the constructor */ + public static final int DEFAULT_CAPACITY = 8; + + /** Default load factor. Used if not specified in the constructor */ + public static final float DEFAULT_LOAD_FACTOR = 0.5f; + + /** + * Placeholder for null values, so we can use the actual null to mean available. (Better than + * using a placeholder for available: less references for GC processing.) + */ + private static final Object NULL_VALUE = new Object(); + + /** The maximum number of elements allowed without allocating more space. */ + private int maxSize; + + /** The load factor for the map. Used to calculate {@link #maxSize}. */ + private final float loadFactor; + + private int[] keys; + private V[] values; + private int size; + private int mask; + + private final Set keySet = new KeySet(); + private final Set> entrySet = new EntrySet(); + private final Iterable> entries = PrimitiveIterator::new; + + public SynchronizedIntObjectHashMap() { + this(DEFAULT_CAPACITY, DEFAULT_LOAD_FACTOR); + } + + public SynchronizedIntObjectHashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + public SynchronizedIntObjectHashMap(int initialCapacity, float loadFactor) { + if (loadFactor <= 0.0f || loadFactor > 1.0f) { + // Cannot exceed 1 because we can never store more than capacity elements; + // using a bigger loadFactor would trigger rehashing before the desired load is reached. + throw new IllegalArgumentException("loadFactor must be > 0 and <= 1"); + } + + this.loadFactor = loadFactor; + + // Adjust the initial capacity if necessary. + int capacity = safeFindNextPositivePowerOfTwo(initialCapacity); + mask = capacity - 1; + + // Allocate the arrays. + keys = new int[capacity]; + @SuppressWarnings({"unchecked", "SuspiciousArrayCast"}) + V[] temp = (V[]) new Object[capacity]; + values = temp; + + // Initialize the maximum size value. + maxSize = calcMaxSize(capacity); + } + + private static T toExternal(T value) { + assert value != null : "null is not a legitimate internal value. Concurrent Modification?"; + return value == NULL_VALUE ? null : value; + } + + @SuppressWarnings("unchecked") + private static T toInternal(T value) { + return value == null ? (T) NULL_VALUE : value; + } + + public synchronized V[] getValuesCopy() { + V[] values = this.values; + return Arrays.copyOf(values, values.length); + } + + @Override + public synchronized V get(int key) { + int index = indexOf(key); + return index == -1 ? null : toExternal(values[index]); + } + + @Override + public synchronized V put(int key, V value) { + int startIndex = hashIndex(key); + int index = startIndex; + + for (; ; ) { + if (values[index] == null) { + // Found empty slot, use it. + keys[index] = key; + values[index] = toInternal(value); + growSize(); + return null; + } + if (keys[index] == key) { + // Found existing entry with this key, just replace the value. + V previousValue = values[index]; + values[index] = toInternal(value); + return toExternal(previousValue); + } + + // Conflict, keep probing ... + if ((index = probeNext(index)) == startIndex) { + // Can only happen if the map was full at MAX_ARRAY_SIZE and couldn't grow. + throw new IllegalStateException("Unable to insert"); + } + } + } + + @Override + public synchronized void putAll(Map sourceMap) { + if (sourceMap instanceof SynchronizedIntObjectHashMap) { + // Optimization - iterate through the arrays. + @SuppressWarnings("unchecked") + SynchronizedIntObjectHashMap source = (SynchronizedIntObjectHashMap) sourceMap; + for (int i = 0; i < source.values.length; ++i) { + V sourceValue = source.values[i]; + if (sourceValue != null) { + put(source.keys[i], sourceValue); + } + } + return; + } + + // Otherwise, just add each entry. + for (Entry entry : sourceMap.entrySet()) { + put(entry.getKey(), entry.getValue()); + } + } + + @Override + public synchronized V remove(int key) { + int index = indexOf(key); + if (index == -1) { + return null; + } + + V prev = values[index]; + removeAt(index); + return toExternal(prev); + } + + @Override + public synchronized int size() { + return size; + } + + @Override + public synchronized boolean isEmpty() { + return size == 0; + } + + @Override + public synchronized void clear() { + Arrays.fill(keys, (int) 0); + Arrays.fill(values, null); + size = 0; + } + + @Override + public synchronized boolean containsKey(int key) { + return indexOf(key) >= 0; + } + + @Override + public synchronized boolean containsValue(Object value) { + @SuppressWarnings("unchecked") + V v1 = toInternal((V) value); + for (V v2 : values) { + // The map supports null values; this will be matched as NULL_VALUE.equals(NULL_VALUE). + if (v2 != null && v2.equals(v1)) { + return true; + } + } + return false; + } + + @Override + public synchronized Iterable> entries() { + return entries; + } + + @Override + public synchronized Collection values() { + return new AbstractCollection() { + @Override + public Iterator iterator() { + return new Iterator() { + final PrimitiveIterator iter = new PrimitiveIterator(); + + @Override + public boolean hasNext() { + return iter.hasNext(); + } + + @Override + public V next() { + return iter.next().value(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + + @Override + public int size() { + return size; + } + }; + } + + @Override + public synchronized int hashCode() { + // Hashcode is based on all non-zero, valid keys. We have to scan the whole keys + // array, which may have different lengths for two maps of same size(), so the + // capacity cannot be used as input for hashing but the size can. + int hash = size; + for (int key : keys) { + // 0 can be a valid key or unused slot, but won't impact the hashcode in either case. + // This way we can use a cheap loop without conditionals, or hard-to-unroll operations, + // or the devastatingly bad memory locality of visiting value objects. + // Also, it's important to use a hash function that does not depend on the ordering + // of terms, only their values; since the map is an unordered collection and + // entries can end up in different positions in different maps that have the same + // elements, but with different history of puts/removes, due to conflicts. + hash ^= hashCode(key); + } + return hash; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (!(obj instanceof IntObjectMap)) { + return false; + } + @SuppressWarnings("rawtypes") + IntObjectMap other = (IntObjectMap) obj; + synchronized (this) { + if (size != other.size()) { + return false; + } + for (int i = 0; i < values.length; ++i) { + V value = values[i]; + if (value != null) { + int key = keys[i]; + Object otherValue = other.get(key); + if (value == NULL_VALUE) { + if (otherValue != null) { + return false; + } + } else if (!value.equals(otherValue)) { + return false; + } + } + } + } + return true; + } + + @Override + public synchronized boolean containsKey(Object key) { + return containsKey(objectToKey(key)); + } + + @Override + public synchronized V get(Object key) { + return get(objectToKey(key)); + } + + @Override + public synchronized V put(Integer key, V value) { + return put(objectToKey(key), value); + } + + @Override + public synchronized V remove(Object key) { + return remove(objectToKey(key)); + } + + @Override + public synchronized Set keySet() { + return keySet; + } + + @Override + public synchronized Set> entrySet() { + return entrySet; + } + + private int objectToKey(Object key) { + return (int) ((Integer) key).intValue(); + } + + /** + * Locates the index for the given key. This method probes using double hashing. + * + * @param key the key for an entry in the map. + * @return the index where the key was found, or {@code -1} if no entry is found for that key. + */ + private int indexOf(int key) { + int startIndex = hashIndex(key); + int index = startIndex; + + for (; ; ) { + if (values[index] == null) { + // It's available, so no chance that this value exists anywhere in the map. + return -1; + } + if (key == keys[index]) { + return index; + } + + // Conflict, keep probing ... + if ((index = probeNext(index)) == startIndex) { + return -1; + } + } + } + + /** Returns the hashed index for the given key. */ + private int hashIndex(int key) { + // The array lengths are always a power of two, so we can use a bitmask to stay inside the array + // bounds. + return hashCode(key) & mask; + } + + /** Returns the hash code for the key. */ + private static int hashCode(int key) { + return (int) key; + } + + /** Get the next sequential index after {@code index} and wraps if necessary. */ + private int probeNext(int index) { + // The array lengths are always a power of two, so we can use a bitmask to stay inside the array + // bounds. + return (index + 1) & mask; + } + + /** Grows the map size after an insertion. If necessary, performs a rehash of the map. */ + private void growSize() { + size++; + + if (size > maxSize) { + if (keys.length == Integer.MAX_VALUE) { + throw new IllegalStateException("Max capacity reached at size=" + size); + } + + // Double the capacity. + rehash(keys.length << 1); + } + } + + /** + * Removes entry at the given index position. Also performs opportunistic, incremental rehashing + * if necessary to not break conflict chains. + * + * @param index the index position of the element to remove. + * @return {@code true} if the next item was moved back. {@code false} otherwise. + */ + private boolean removeAt(final int index) { + --size; + // Clearing the key is not strictly necessary (for GC like in a regular collection), + // but recommended for security. The memory location is still fresh in the cache anyway. + keys[index] = 0; + values[index] = null; + + // In the interval from index to the next available entry, the arrays may have entries + // that are displaced from their base position due to prior conflicts. Iterate these + // entries and move them back if possible, optimizing future lookups. + // Knuth Section 6.4 Algorithm R, also used by the JDK's IdentityHashMap. + + int nextFree = index; + int i = probeNext(index); + for (V value = values[i]; value != null; value = values[i = probeNext(i)]) { + int key = keys[i]; + int bucket = hashIndex(key); + if (i < bucket && (bucket <= nextFree || nextFree <= i) + || bucket <= nextFree && nextFree <= i) { + // Move the displaced entry "back" to the first available position. + keys[nextFree] = key; + values[nextFree] = value; + // Put the first entry after the displaced entry + keys[i] = 0; + values[i] = null; + nextFree = i; + } + } + return nextFree != index; + } + + /** Calculates the maximum size allowed before rehashing. */ + private int calcMaxSize(int capacity) { + // Clip the upper bound so that there will always be at least one available slot. + int upperBound = capacity - 1; + return Math.min(upperBound, (int) (capacity * loadFactor)); + } + + /** + * Rehashes the map for the given capacity. + * + * @param newCapacity the new capacity for the map. + */ + private void rehash(int newCapacity) { + int[] oldKeys = keys; + V[] oldVals = values; + + keys = new int[newCapacity]; + @SuppressWarnings({"unchecked", "SuspiciousArrayCast"}) + V[] temp = (V[]) new Object[newCapacity]; + values = temp; + + maxSize = calcMaxSize(newCapacity); + mask = newCapacity - 1; + + // Insert to the new arrays. + for (int i = 0; i < oldVals.length; ++i) { + V oldVal = oldVals[i]; + if (oldVal != null) { + // Inlined put(), but much simpler: we don't need to worry about + // duplicated keys, growing/rehashing, or failing to insert. + int oldKey = oldKeys[i]; + int index = hashIndex(oldKey); + + for (; ; ) { + if (values[index] == null) { + keys[index] = oldKey; + values[index] = oldVal; + break; + } + + // Conflict, keep probing. Can wrap around, but never reaches startIndex again. + index = probeNext(index); + } + } + } + } + + @Override + public synchronized String toString() { + if (isEmpty()) { + return "{}"; + } + StringBuilder sb = new StringBuilder(4 * size); + sb.append('{'); + boolean first = true; + for (int i = 0; i < values.length; ++i) { + V value = values[i]; + if (value != null) { + if (!first) { + sb.append(", "); + } + sb.append(keyToString(keys[i])) + .append('=') + .append(value == this ? "(this Map)" : toExternal(value)); + first = false; + } + } + return sb.append('}').toString(); + } + + /** + * Helper method called by {@link #toString()} in order to convert a single map key into a string. + * This is protected to allow subclasses to override the appearance of a given key. + */ + protected String keyToString(int key) { + return Integer.toString(key); + } + + /** Set implementation for iterating over the entries of the map. */ + private final class EntrySet extends AbstractSet> { + @Override + public Iterator> iterator() { + return new MapIterator(); + } + + @Override + public int size() { + return SynchronizedIntObjectHashMap.this.size(); + } + } + + /** Set implementation for iterating over the keys. */ + private final class KeySet extends AbstractSet { + @Override + public int size() { + return SynchronizedIntObjectHashMap.this.size(); + } + + @Override + public boolean contains(Object o) { + return SynchronizedIntObjectHashMap.this.containsKey(o); + } + + @Override + public boolean remove(Object o) { + return SynchronizedIntObjectHashMap.this.remove(o) != null; + } + + @Override + public boolean retainAll(Collection retainedKeys) { + synchronized (SynchronizedIntObjectHashMap.this) { + boolean changed = false; + for (Iterator> iter = entries().iterator(); iter.hasNext(); ) { + PrimitiveEntry entry = iter.next(); + if (!retainedKeys.contains(entry.key())) { + changed = true; + iter.remove(); + } + } + return changed; + } + } + + @Override + public void clear() { + SynchronizedIntObjectHashMap.this.clear(); + } + + @Override + public Iterator iterator() { + synchronized (SynchronizedIntObjectHashMap.this) { + final Iterator> iter = entrySet.iterator(); + return new Iterator() { + @Override + public boolean hasNext() { + synchronized (SynchronizedIntObjectHashMap.this) { + return iter.hasNext(); + } + } + + @Override + public Integer next() { + synchronized (SynchronizedIntObjectHashMap.this) { + return iter.next().getKey(); + } + } + + @Override + public void remove() { + synchronized (SynchronizedIntObjectHashMap.this) { + iter.remove(); + } + } + }; + } + } + } + + /** + * Iterator over primitive entries. Entry key/values are overwritten by each call to {@link + * #next()}. + */ + private final class PrimitiveIterator implements Iterator>, PrimitiveEntry { + private int prevIndex = -1; + private int nextIndex = -1; + private int entryIndex = -1; + + private void scanNext() { + while (++nextIndex != values.length && values[nextIndex] == null) {} + } + + @Override + public boolean hasNext() { + synchronized (SynchronizedIntObjectHashMap.this) { + if (nextIndex == -1) { + scanNext(); + } + return nextIndex != values.length; + } + } + + @Override + public PrimitiveEntry next() { + synchronized (SynchronizedIntObjectHashMap.this) { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + prevIndex = nextIndex; + scanNext(); + + // Always return the same Entry object, just change its index each time. + entryIndex = prevIndex; + return this; + } + } + + @Override + public void remove() { + synchronized (SynchronizedIntObjectHashMap.this) { + if (prevIndex == -1) { + throw new IllegalStateException("next must be called before each remove."); + } + if (removeAt(prevIndex)) { + // removeAt may move elements "back" in the array if they have been displaced because + // their + // spot in the + // array was occupied when they were inserted. If this occurs then the nextIndex is now + // invalid and + // should instead point to the prevIndex which now holds an element which was "moved + // back". + nextIndex = prevIndex; + } + prevIndex = -1; + } + } + + // Entry implementation. Since this implementation uses a single Entry, we coalesce that + // into the Iterator object (potentially making loop optimization much easier). + + @Override + public int key() { + synchronized (SynchronizedIntObjectHashMap.this) { + return keys[entryIndex]; + } + } + + @Override + public V value() { + synchronized (SynchronizedIntObjectHashMap.this) { + return toExternal(values[entryIndex]); + } + } + + @Override + public void setValue(V value) { + synchronized (SynchronizedIntObjectHashMap.this) { + values[entryIndex] = toInternal(value); + } + } + } + + /** Iterator used by the {@link Map} interface. */ + private final class MapIterator implements Iterator> { + private final PrimitiveIterator iter = new PrimitiveIterator(); + + @Override + public boolean hasNext() { + synchronized (SynchronizedIntObjectHashMap.this) { + return iter.hasNext(); + } + } + + @Override + public Entry next() { + synchronized (SynchronizedIntObjectHashMap.this) { + if (!hasNext()) { + throw new NoSuchElementException(); + } + + iter.next(); + + return new MapEntry(iter.entryIndex); + } + } + + @Override + public void remove() { + synchronized (SynchronizedIntObjectHashMap.this) { + iter.remove(); + } + } + } + + /** A single entry in the map. */ + final class MapEntry implements Entry { + private final int entryIndex; + + MapEntry(int entryIndex) { + this.entryIndex = entryIndex; + } + + @Override + public Integer getKey() { + synchronized (SynchronizedIntObjectHashMap.this) { + verifyExists(); + return keys[entryIndex]; + } + } + + @Override + public V getValue() { + synchronized (SynchronizedIntObjectHashMap.this) { + verifyExists(); + return toExternal(values[entryIndex]); + } + } + + @Override + public V setValue(V value) { + synchronized (SynchronizedIntObjectHashMap.this) { + verifyExists(); + V prevValue = toExternal(values[entryIndex]); + values[entryIndex] = toInternal(value); + return prevValue; + } + } + + private void verifyExists() { + if (values[entryIndex] == null) { + throw new IllegalStateException("The map entry has been removed"); + } + } + } +} 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 1affeb8fd..dfcc13a64 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -17,6 +17,7 @@ package io.rsocket.internal; import io.netty.util.ReferenceCounted; +import io.rsocket.internal.jctools.queues.MpscUnboundedArrayQueue; import java.util.Objects; import java.util.Queue; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; @@ -65,7 +66,7 @@ public final class UnboundedProcessor extends FluxProcessor volatile boolean outputFused; public UnboundedProcessor() { - this.queue = Queues.unboundedMultiproducer().get(); + this.queue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseLinkedQueue.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseLinkedQueue.java new file mode 100644 index 000000000..6939b0f7a --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseLinkedQueue.java @@ -0,0 +1,258 @@ +/* + * 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.internal.jctools.queues; + +import static io.rsocket.internal.jctools.util.UnsafeAccess.UNSAFE; +import static io.rsocket.internal.jctools.util.UnsafeAccess.fieldOffset; + +import java.util.AbstractQueue; +import java.util.Iterator; + +abstract class BaseLinkedQueuePad0 extends AbstractQueue implements MessagePassingQueue { + long p00, p01, p02, p03, p04, p05, p06, p07; + long p10, p11, p12, p13, p14, p15, p16; +} + +// $gen:ordered-fields +abstract class BaseLinkedQueueProducerNodeRef extends BaseLinkedQueuePad0 { + static final long P_NODE_OFFSET = + fieldOffset(BaseLinkedQueueProducerNodeRef.class, "producerNode"); + + private LinkedQueueNode producerNode; + + final void spProducerNode(LinkedQueueNode newValue) { + producerNode = newValue; + } + + @SuppressWarnings("unchecked") + final LinkedQueueNode lvProducerNode() { + return (LinkedQueueNode) UNSAFE.getObjectVolatile(this, P_NODE_OFFSET); + } + + @SuppressWarnings("unchecked") + final boolean casProducerNode(LinkedQueueNode expect, LinkedQueueNode newValue) { + return UNSAFE.compareAndSwapObject(this, P_NODE_OFFSET, expect, newValue); + } + + final LinkedQueueNode lpProducerNode() { + return producerNode; + } +} + +abstract class BaseLinkedQueuePad1 extends BaseLinkedQueueProducerNodeRef { + long p01, p02, p03, p04, p05, p06, p07; + long p10, p11, p12, p13, p14, p15, p16, p17; +} + +// $gen:ordered-fields +abstract class BaseLinkedQueueConsumerNodeRef extends BaseLinkedQueuePad1 { + private static final long C_NODE_OFFSET = + fieldOffset(BaseLinkedQueueConsumerNodeRef.class, "consumerNode"); + + private LinkedQueueNode consumerNode; + + final void spConsumerNode(LinkedQueueNode newValue) { + consumerNode = newValue; + } + + @SuppressWarnings("unchecked") + final LinkedQueueNode lvConsumerNode() { + return (LinkedQueueNode) UNSAFE.getObjectVolatile(this, C_NODE_OFFSET); + } + + final LinkedQueueNode lpConsumerNode() { + return consumerNode; + } +} + +abstract class BaseLinkedQueuePad2 extends BaseLinkedQueueConsumerNodeRef { + long p01, p02, p03, p04, p05, p06, p07; + long p10, p11, p12, p13, p14, p15, p16, p17; +} + +/** + * A base data structure for concurrent linked queues. For convenience also pulled in common single + * consumer methods since at this time there's no plan to implement MC. + * + * @param + * @author nitsanw + */ +abstract class BaseLinkedQueue extends BaseLinkedQueuePad2 { + + @Override + public final Iterator iterator() { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + return this.getClass().getName(); + } + + protected final LinkedQueueNode newNode() { + return new LinkedQueueNode(); + } + + protected final LinkedQueueNode newNode(E e) { + return new LinkedQueueNode(e); + } + + /** + * {@inheritDoc}
+ * + *

IMPLEMENTATION NOTES:
+ * This is an O(n) operation as we run through all the nodes and count them.
+ * The accuracy of the value returned by this method is subject to races with producer/consumer + * threads. In particular when racing with the consumer thread this method may under estimate the + * size.
+ * + * @see java.util.Queue#size() + */ + @Override + public final int size() { + // Read consumer first, this is important because if the producer is node is 'older' than the + // consumer + // the consumer may overtake it (consume past it) invalidating the 'snapshot' notion of size. + LinkedQueueNode chaserNode = lvConsumerNode(); + LinkedQueueNode producerNode = lvProducerNode(); + int size = 0; + // must chase the nodes all the way to the producer node, but there's no need to count beyond + // expected head. + while (chaserNode != producerNode + && // don't go passed producer node + chaserNode != null + && // stop at last node + size < Integer.MAX_VALUE) // stop at max int + { + LinkedQueueNode next; + next = chaserNode.lvNext(); + // check if this node has been consumed, if so return what we have + if (next == chaserNode) { + return size; + } + chaserNode = next; + size++; + } + return size; + } + + /** + * {@inheritDoc}
+ * + *

IMPLEMENTATION NOTES:
+ * Queue is empty when producerNode is the same as consumerNode. An alternative implementation + * would be to observe the producerNode.value is null, which also means an empty queue because + * only the consumerNode.value is allowed to be null. + * + * @see MessagePassingQueue#isEmpty() + */ + @Override + public final boolean isEmpty() { + return lvConsumerNode() == lvProducerNode(); + } + + protected E getSingleConsumerNodeValue( + LinkedQueueNode currConsumerNode, LinkedQueueNode nextNode) { + // we have to null out the value because we are going to hang on to the node + final E nextValue = nextNode.getAndNullValue(); + + // Fix up the next ref of currConsumerNode to prevent promoted nodes from keeping new ones + // alive. + // We use a reference to self instead of null because null is already a meaningful value (the + // next of + // producer node is null). + currConsumerNode.soNext(currConsumerNode); + spConsumerNode(nextNode); + // currConsumerNode is now no longer referenced and can be collected + return nextValue; + } + + @Override + public E relaxedPoll() { + final LinkedQueueNode currConsumerNode = lpConsumerNode(); + final LinkedQueueNode nextNode = currConsumerNode.lvNext(); + if (nextNode != null) { + return getSingleConsumerNodeValue(currConsumerNode, nextNode); + } + return null; + } + + @Override + public E relaxedPeek() { + final LinkedQueueNode nextNode = lpConsumerNode().lvNext(); + if (nextNode != null) { + return nextNode.lpValue(); + } + return null; + } + + @Override + public boolean relaxedOffer(E e) { + return offer(e); + } + + @Override + public int drain(Consumer c) { + long result = 0; // use long to force safepoint into loop below + int drained; + do { + drained = drain(c, 4096); + result += drained; + } while (drained == 4096 && result <= Integer.MAX_VALUE - 4096); + return (int) result; + } + + @Override + public int drain(Consumer c, int limit) { + LinkedQueueNode chaserNode = this.lpConsumerNode(); + for (int i = 0; i < limit; i++) { + final LinkedQueueNode nextNode = chaserNode.lvNext(); + + if (nextNode == null) { + return i; + } + // we have to null out the value because we are going to hang on to the node + final E nextValue = getSingleConsumerNodeValue(chaserNode, nextNode); + chaserNode = nextNode; + c.accept(nextValue); + } + return limit; + } + + @Override + public void drain(Consumer c, WaitStrategy wait, ExitCondition exit) { + LinkedQueueNode chaserNode = this.lpConsumerNode(); + int idleCounter = 0; + while (exit.keepRunning()) { + for (int i = 0; i < 4096; i++) { + final LinkedQueueNode nextNode = chaserNode.lvNext(); + if (nextNode == null) { + idleCounter = wait.idle(idleCounter); + continue; + } + + idleCounter = 0; + // we have to null out the value because we are going to hang on to the node + final E nextValue = getSingleConsumerNodeValue(chaserNode, nextNode); + chaserNode = nextNode; + c.accept(nextValue); + } + } + } + + @Override + public int capacity() { + return UNBOUNDED_CAPACITY; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseMpscLinkedArrayQueue.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseMpscLinkedArrayQueue.java new file mode 100644 index 000000000..635779df3 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/BaseMpscLinkedArrayQueue.java @@ -0,0 +1,663 @@ +/* + * 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.internal.jctools.queues; + +import static io.rsocket.internal.jctools.queues.CircularArrayOffsetCalculator.allocate; +import static io.rsocket.internal.jctools.queues.LinkedArrayQueueUtil.length; +import static io.rsocket.internal.jctools.queues.LinkedArrayQueueUtil.modifiedCalcElementOffset; +import static io.rsocket.internal.jctools.util.UnsafeAccess.UNSAFE; +import static io.rsocket.internal.jctools.util.UnsafeAccess.fieldOffset; +import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.calcElementOffset; +import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.lvElement; +import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.soElement; + +import io.rsocket.internal.jctools.queues.IndexedQueueSizeUtil.IndexedQueue; +import io.rsocket.internal.jctools.util.PortableJvmInfo; +import io.rsocket.internal.jctools.util.Pow2; +import io.rsocket.internal.jctools.util.RangeUtil; +import java.util.AbstractQueue; +import java.util.Iterator; + +abstract class BaseMpscLinkedArrayQueuePad1 extends AbstractQueue implements IndexedQueue { + long p01, p02, p03, p04, p05, p06, p07; + long p10, p11, p12, p13, p14, p15, p16, p17; +} + +// $gen:ordered-fields +abstract class BaseMpscLinkedArrayQueueProducerFields extends BaseMpscLinkedArrayQueuePad1 { + private static final long P_INDEX_OFFSET = + fieldOffset(BaseMpscLinkedArrayQueueProducerFields.class, "producerIndex"); + + private volatile long producerIndex; + + @Override + public final long lvProducerIndex() { + return producerIndex; + } + + final void soProducerIndex(long newValue) { + UNSAFE.putOrderedLong(this, P_INDEX_OFFSET, newValue); + } + + final boolean casProducerIndex(long expect, long newValue) { + return UNSAFE.compareAndSwapLong(this, P_INDEX_OFFSET, expect, newValue); + } +} + +abstract class BaseMpscLinkedArrayQueuePad2 extends BaseMpscLinkedArrayQueueProducerFields { + long p01, p02, p03, p04, p05, p06, p07; + long p10, p11, p12, p13, p14, p15, p16, p17; +} + +// $gen:ordered-fields +abstract class BaseMpscLinkedArrayQueueConsumerFields extends BaseMpscLinkedArrayQueuePad2 { + private static final long C_INDEX_OFFSET = + fieldOffset(BaseMpscLinkedArrayQueueConsumerFields.class, "consumerIndex"); + + private volatile long consumerIndex; + protected long consumerMask; + protected E[] consumerBuffer; + + @Override + public final long lvConsumerIndex() { + return consumerIndex; + } + + final long lpConsumerIndex() { + return UNSAFE.getLong(this, C_INDEX_OFFSET); + } + + final void soConsumerIndex(long newValue) { + UNSAFE.putOrderedLong(this, C_INDEX_OFFSET, newValue); + } +} + +abstract class BaseMpscLinkedArrayQueuePad3 extends BaseMpscLinkedArrayQueueConsumerFields { + long p0, p1, p2, p3, p4, p5, p6, p7; + long p10, p11, p12, p13, p14, p15, p16, p17; +} + +// $gen:ordered-fields +abstract class BaseMpscLinkedArrayQueueColdProducerFields + extends BaseMpscLinkedArrayQueuePad3 { + private static final long P_LIMIT_OFFSET = + fieldOffset(BaseMpscLinkedArrayQueueColdProducerFields.class, "producerLimit"); + + private volatile long producerLimit; + protected long producerMask; + protected E[] producerBuffer; + + final long lvProducerLimit() { + return producerLimit; + } + + final boolean casProducerLimit(long expect, long newValue) { + return UNSAFE.compareAndSwapLong(this, P_LIMIT_OFFSET, expect, newValue); + } + + final void soProducerLimit(long newValue) { + UNSAFE.putOrderedLong(this, P_LIMIT_OFFSET, newValue); + } +} + +/** + * An MPSC array queue which starts at initialCapacity and grows to maxCapacity in + * linked chunks of the initial size. The queue grows only when the current buffer is full and + * elements are not copied on resize, instead a link to the new buffer is stored in the old buffer + * for the consumer to follow.
+ * + * @param + */ +public abstract class BaseMpscLinkedArrayQueue + extends BaseMpscLinkedArrayQueueColdProducerFields + implements MessagePassingQueue, QueueProgressIndicators { + // No post padding here, subclasses must add + private static final Object JUMP = new Object(); + private static final Object BUFFER_CONSUMED = new Object(); + private static final int CONTINUE_TO_P_INDEX_CAS = 0; + private static final int RETRY = 1; + private static final int QUEUE_FULL = 2; + private static final int QUEUE_RESIZE = 3; + + /** + * @param initialCapacity the queue initial capacity. If chunk size is fixed this will be the + * chunk size. Must be 2 or more. + */ + public BaseMpscLinkedArrayQueue(final int initialCapacity) { + RangeUtil.checkGreaterThanOrEqual(initialCapacity, 2, "initialCapacity"); + + int p2capacity = Pow2.roundToPowerOfTwo(initialCapacity); + // leave lower bit of mask clear + long mask = (p2capacity - 1) << 1; + // need extra element to point at next array + E[] buffer = allocate(p2capacity + 1); + producerBuffer = buffer; + producerMask = mask; + consumerBuffer = buffer; + consumerMask = mask; + soProducerLimit(mask); // we know it's all empty to start with + } + + @Override + public final int size() { + // NOTE: because indices are on even numbers we cannot use the size util. + + /* + * It is possible for a thread to be interrupted or reschedule between the read of the producer and + * consumer indices, therefore protection is required to ensure size is within valid range. In the + * event of concurrent polls/offers to this method the size is OVER estimated as we read consumer + * index BEFORE the producer index. + */ + long after = lvConsumerIndex(); + long size; + while (true) { + final long before = after; + final long currentProducerIndex = lvProducerIndex(); + after = lvConsumerIndex(); + if (before == after) { + size = ((currentProducerIndex - after) >> 1); + break; + } + } + // Long overflow is impossible, so size is always positive. Integer overflow is possible for the + // unbounded + // indexed queues. + if (size > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else { + return (int) size; + } + } + + @Override + public final boolean isEmpty() { + // Order matters! + // Loading consumer before producer allows for producer increments after consumer index is read. + // This ensures this method is conservative in it's estimate. Note that as this is an MPMC there + // is + // nothing we can do to make this an exact method. + return (this.lvConsumerIndex() == this.lvProducerIndex()); + } + + @Override + public String toString() { + return this.getClass().getName(); + } + + @Override + public boolean offer(final E e) { + if (null == e) { + throw new NullPointerException(); + } + + long mask; + E[] buffer; + long pIndex; + + while (true) { + long producerLimit = lvProducerLimit(); + pIndex = lvProducerIndex(); + // lower bit is indicative of resize, if we see it we spin until it's cleared + if ((pIndex & 1) == 1) { + continue; + } + // pIndex is even (lower bit is 0) -> actual index is (pIndex >> 1) + + // mask/buffer may get changed by resizing -> only use for array access after successful CAS. + mask = this.producerMask; + buffer = this.producerBuffer; + // a successful CAS ties the ordering, lv(pIndex) - [mask/buffer] -> cas(pIndex) + + // assumption behind this optimization is that queue is almost always empty or near empty + if (producerLimit <= pIndex) { + int result = offerSlowPath(mask, pIndex, producerLimit); + switch (result) { + case CONTINUE_TO_P_INDEX_CAS: + break; + case RETRY: + continue; + case QUEUE_FULL: + return false; + case QUEUE_RESIZE: + resize(mask, buffer, pIndex, e, null); + return true; + } + } + + if (casProducerIndex(pIndex, pIndex + 2)) { + break; + } + } + // INDEX visible before ELEMENT + final long offset = modifiedCalcElementOffset(pIndex, mask); + soElement(buffer, offset, e); // release element e + return true; + } + + /** + * {@inheritDoc} + * + *

This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public E poll() { + final E[] buffer = consumerBuffer; + final long index = lpConsumerIndex(); + final long mask = consumerMask; + + final long offset = modifiedCalcElementOffset(index, mask); + Object e = lvElement(buffer, offset); // LoadLoad + if (e == null) { + if (index != lvProducerIndex()) { + // poll() == null iff queue is empty, null element is not strong enough indicator, so we + // must + // check the producer index. If the queue is indeed not empty we spin until element is + // visible. + do { + e = lvElement(buffer, offset); + } while (e == null); + } else { + return null; + } + } + + if (e == JUMP) { + final E[] nextBuffer = nextBuffer(buffer, mask); + return newBufferPoll(nextBuffer, index); + } + + soElement(buffer, offset, null); // release element null + soConsumerIndex(index + 2); // release cIndex + return (E) e; + } + + /** + * {@inheritDoc} + * + *

This implementation is correct for single consumer thread use only. + */ + @SuppressWarnings("unchecked") + @Override + public E peek() { + final E[] buffer = consumerBuffer; + final long index = lpConsumerIndex(); + final long mask = consumerMask; + + final long offset = modifiedCalcElementOffset(index, mask); + Object e = lvElement(buffer, offset); // LoadLoad + if (e == null && index != lvProducerIndex()) { + // peek() == null iff queue is empty, null element is not strong enough indicator, so we must + // check the producer index. If the queue is indeed not empty we spin until element is + // visible. + do { + e = lvElement(buffer, offset); + } while (e == null); + } + if (e == JUMP) { + return newBufferPeek(nextBuffer(buffer, mask), index); + } + return (E) e; + } + + /** We do not inline resize into this method because we do not resize on fill. */ + private int offerSlowPath(long mask, long pIndex, long producerLimit) { + final long cIndex = lvConsumerIndex(); + long bufferCapacity = getCurrentBufferCapacity(mask); + + if (cIndex + bufferCapacity > pIndex) { + if (!casProducerLimit(producerLimit, cIndex + bufferCapacity)) { + // retry from top + return RETRY; + } else { + // continue to pIndex CAS + return CONTINUE_TO_P_INDEX_CAS; + } + } + // full and cannot grow + else if (availableInQueue(pIndex, cIndex) <= 0) { + // offer should return false; + return QUEUE_FULL; + } + // grab index for resize -> set lower bit + else if (casProducerIndex(pIndex, pIndex + 1)) { + // trigger a resize + return QUEUE_RESIZE; + } else { + // failed resize attempt, retry from top + return RETRY; + } + } + + /** @return available elements in queue * 2 */ + protected abstract long availableInQueue(long pIndex, long cIndex); + + @SuppressWarnings("unchecked") + private E[] nextBuffer(final E[] buffer, final long mask) { + final long offset = nextArrayOffset(mask); + final E[] nextBuffer = (E[]) lvElement(buffer, offset); + consumerBuffer = nextBuffer; + consumerMask = (length(nextBuffer) - 2) << 1; + soElement(buffer, offset, BUFFER_CONSUMED); + return nextBuffer; + } + + private long nextArrayOffset(long mask) { + return modifiedCalcElementOffset(mask + 2, Long.MAX_VALUE); + } + + private E newBufferPoll(E[] nextBuffer, long index) { + final long offset = modifiedCalcElementOffset(index, consumerMask); + final E n = lvElement(nextBuffer, offset); // LoadLoad + if (n == null) { + throw new IllegalStateException("new buffer must have at least one element"); + } + soElement(nextBuffer, offset, null); // StoreStore + soConsumerIndex(index + 2); + return n; + } + + private E newBufferPeek(E[] nextBuffer, long index) { + final long offset = modifiedCalcElementOffset(index, consumerMask); + final E n = lvElement(nextBuffer, offset); // LoadLoad + if (null == n) { + throw new IllegalStateException("new buffer must have at least one element"); + } + return n; + } + + @Override + public long currentProducerIndex() { + return lvProducerIndex() / 2; + } + + @Override + public long currentConsumerIndex() { + return lvConsumerIndex() / 2; + } + + @Override + public abstract int capacity(); + + @Override + public boolean relaxedOffer(E e) { + return offer(e); + } + + @SuppressWarnings("unchecked") + @Override + public E relaxedPoll() { + final E[] buffer = consumerBuffer; + final long index = lpConsumerIndex(); + final long mask = consumerMask; + + final long offset = modifiedCalcElementOffset(index, mask); + Object e = lvElement(buffer, offset); // LoadLoad + if (e == null) { + return null; + } + if (e == JUMP) { + final E[] nextBuffer = nextBuffer(buffer, mask); + return newBufferPoll(nextBuffer, index); + } + soElement(buffer, offset, null); + soConsumerIndex(index + 2); + return (E) e; + } + + @SuppressWarnings("unchecked") + @Override + public E relaxedPeek() { + final E[] buffer = consumerBuffer; + final long index = lpConsumerIndex(); + final long mask = consumerMask; + + final long offset = modifiedCalcElementOffset(index, mask); + Object e = lvElement(buffer, offset); // LoadLoad + if (e == JUMP) { + return newBufferPeek(nextBuffer(buffer, mask), index); + } + return (E) e; + } + + @Override + public int fill(Supplier s) { + long result = + 0; // result is a long because we want to have a safepoint check at regular intervals + final int capacity = capacity(); + do { + final int filled = fill(s, PortableJvmInfo.RECOMENDED_OFFER_BATCH); + if (filled == 0) { + return (int) result; + } + result += filled; + } while (result <= capacity); + return (int) result; + } + + @Override + public int fill(Supplier s, int batchSize) { + long mask; + E[] buffer; + long pIndex; + int claimedSlots; + while (true) { + long producerLimit = lvProducerLimit(); + pIndex = lvProducerIndex(); + // lower bit is indicative of resize, if we see it we spin until it's cleared + if ((pIndex & 1) == 1) { + continue; + } + // pIndex is even (lower bit is 0) -> actual index is (pIndex >> 1) + + // NOTE: mask/buffer may get changed by resizing -> only use for array access after successful + // CAS. + // Only by virtue offloading them between the lvProducerIndex and a successful + // casProducerIndex are they + // safe to use. + mask = this.producerMask; + buffer = this.producerBuffer; + // a successful CAS ties the ordering, lv(pIndex) -> [mask/buffer] -> cas(pIndex) + + // we want 'limit' slots, but will settle for whatever is visible to 'producerLimit' + long batchIndex = Math.min(producerLimit, pIndex + 2 * batchSize); + + if (pIndex >= producerLimit || producerLimit < batchIndex) { + int result = offerSlowPath(mask, pIndex, producerLimit); + switch (result) { + case CONTINUE_TO_P_INDEX_CAS: + // offer slow path verifies only one slot ahead, we cannot rely on indication here + case RETRY: + continue; + case QUEUE_FULL: + return 0; + case QUEUE_RESIZE: + resize(mask, buffer, pIndex, null, s); + return 1; + } + } + + // claim limit slots at once + if (casProducerIndex(pIndex, batchIndex)) { + claimedSlots = (int) ((batchIndex - pIndex) / 2); + break; + } + } + + for (int i = 0; i < claimedSlots; i++) { + final long offset = modifiedCalcElementOffset(pIndex + 2 * i, mask); + soElement(buffer, offset, s.get()); + } + return claimedSlots; + } + + @Override + public void fill(Supplier s, WaitStrategy w, ExitCondition exit) { + + while (exit.keepRunning()) { + if (fill(s, PortableJvmInfo.RECOMENDED_OFFER_BATCH) == 0) { + int idleCounter = 0; + while (exit.keepRunning() && fill(s, PortableJvmInfo.RECOMENDED_OFFER_BATCH) == 0) { + idleCounter = w.idle(idleCounter); + } + } + } + } + + @Override + public int drain(Consumer c) { + return drain(c, capacity()); + } + + @Override + public int drain(final Consumer c, final int limit) { + // Impl note: there are potentially some small gains to be had by manually inlining + // relaxedPoll() and hoisting + // reused fields out to reduce redundant reads. + int i = 0; + E m; + for (; i < limit && (m = relaxedPoll()) != null; i++) { + c.accept(m); + } + return i; + } + + @Override + public void drain(Consumer c, WaitStrategy w, ExitCondition exit) { + int idleCounter = 0; + while (exit.keepRunning()) { + E e = relaxedPoll(); + if (e == null) { + idleCounter = w.idle(idleCounter); + continue; + } + idleCounter = 0; + c.accept(e); + } + } + + /** + * Get an iterator for this queue. This method is thread safe. + * + *

The iterator provides a best-effort snapshot of the elements in the queue. The returned + * iterator is not guaranteed to return elements in queue order, and races with the consumer + * thread may cause gaps in the sequence of returned elements. Like {link #relaxedPoll}, the + * iterator may not immediately return newly inserted elements. + * + * @return The iterator. + */ + @Override + public Iterator iterator() { + return new WeakIterator(); + } + + private final class WeakIterator implements Iterator { + + private long nextIndex; + private E nextElement; + private E[] currentBuffer; + private int currentBufferLength; + + WeakIterator() { + setBuffer(consumerBuffer); + nextElement = getNext(); + } + + @Override + public boolean hasNext() { + return nextElement != null; + } + + @Override + public E next() { + E e = nextElement; + nextElement = getNext(); + return e; + } + + private void setBuffer(E[] buffer) { + this.currentBuffer = buffer; + this.currentBufferLength = length(buffer); + this.nextIndex = 0; + } + + private E getNext() { + while (true) { + while (nextIndex < currentBufferLength - 1) { + long offset = calcElementOffset(nextIndex++); + E e = lvElement(currentBuffer, offset); + if (e != null && e != JUMP) { + return e; + } + } + long offset = calcElementOffset(currentBufferLength - 1); + Object nextArray = lvElement(currentBuffer, offset); + if (nextArray == BUFFER_CONSUMED) { + // Consumer may have passed us, just jump to the current consumer buffer + setBuffer(consumerBuffer); + } else if (nextArray != null) { + setBuffer((E[]) nextArray); + } else { + return null; + } + } + } + } + + private void resize(long oldMask, E[] oldBuffer, long pIndex, E e, Supplier s) { + assert (e != null && s == null) || (e == null || s != null); + int newBufferLength = getNextBufferSize(oldBuffer); + final E[] newBuffer; + try { + newBuffer = allocate(newBufferLength); + } catch (OutOfMemoryError oom) { + assert lvProducerIndex() == pIndex + 1; + soProducerIndex(pIndex); + throw oom; + } + + producerBuffer = newBuffer; + final int newMask = (newBufferLength - 2) << 1; + producerMask = newMask; + + final long offsetInOld = modifiedCalcElementOffset(pIndex, oldMask); + final long offsetInNew = modifiedCalcElementOffset(pIndex, newMask); + + soElement(newBuffer, offsetInNew, e == null ? s.get() : e); // element in new array + soElement(oldBuffer, nextArrayOffset(oldMask), newBuffer); // buffer linked + + // ASSERT code + final long cIndex = lvConsumerIndex(); + final long availableInQueue = availableInQueue(pIndex, cIndex); + RangeUtil.checkPositive(availableInQueue, "availableInQueue"); + + // Invalidate racing CASs + // We never set the limit beyond the bounds of a buffer + soProducerLimit(pIndex + Math.min(newMask, availableInQueue)); + + // make resize visible to the other producers + soProducerIndex(pIndex + 2); + + // INDEX visible before ELEMENT, consistent with consumer expectation + + // make resize visible to consumer + soElement(oldBuffer, offsetInOld, JUMP); + } + + /** @return next buffer size(inclusive of next array pointer) */ + protected abstract int getNextBufferSize(E[] buffer); + + /** @return current buffer capacity for elements (excluding next pointer and jump entry) * 2 */ + protected abstract long getCurrentBufferCapacity(long mask); +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/CircularArrayOffsetCalculator.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/CircularArrayOffsetCalculator.java new file mode 100644 index 000000000..d746fccbb --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/CircularArrayOffsetCalculator.java @@ -0,0 +1,36 @@ +/* + * 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.internal.jctools.queues; + +import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.REF_ARRAY_BASE; +import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.REF_ELEMENT_SHIFT; + +import io.rsocket.internal.jctools.util.InternalAPI; + +@InternalAPI +public final class CircularArrayOffsetCalculator { + @SuppressWarnings("unchecked") + public static E[] allocate(int capacity) { + return (E[]) new Object[capacity]; + } + + /** + * @param index desirable element index + * @param mask (length - 1) + * @return the offset in bytes within the array for a given index. + */ + public static long calcElementOffset(long index, long mask) { + return REF_ARRAY_BASE + ((index & mask) << REF_ELEMENT_SHIFT); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/IndexedQueueSizeUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/IndexedQueueSizeUtil.java new file mode 100644 index 000000000..1b7d43166 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/IndexedQueueSizeUtil.java @@ -0,0 +1,63 @@ +/* + * 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.internal.jctools.queues; + +import io.rsocket.internal.jctools.util.InternalAPI; + +@InternalAPI +public final class IndexedQueueSizeUtil { + public static int size(IndexedQueue iq) { + /* + * It is possible for a thread to be interrupted or reschedule between the read of the producer and + * consumer indices, therefore protection is required to ensure size is within valid range. In the + * event of concurrent polls/offers to this method the size is OVER estimated as we read consumer + * index BEFORE the producer index. + */ + long after = iq.lvConsumerIndex(); + long size; + while (true) { + final long before = after; + final long currentProducerIndex = iq.lvProducerIndex(); + after = iq.lvConsumerIndex(); + if (before == after) { + size = (currentProducerIndex - after); + break; + } + } + // Long overflow is impossible (), so size is always positive. Integer overflow is possible for + // the unbounded + // indexed queues. + if (size > Integer.MAX_VALUE) { + return Integer.MAX_VALUE; + } else { + return (int) size; + } + } + + public static boolean isEmpty(IndexedQueue iq) { + // Order matters! + // Loading consumer before producer allows for producer increments after consumer index is read. + // This ensures this method is conservative in it's estimate. Note that as this is an MPMC there + // is + // nothing we can do to make this an exact method. + return (iq.lvConsumerIndex() == iq.lvProducerIndex()); + } + + @InternalAPI + public interface IndexedQueue { + long lvConsumerIndex(); + + long lvProducerIndex(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedArrayQueueUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedArrayQueueUtil.java new file mode 100644 index 000000000..5e7831128 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedArrayQueueUtil.java @@ -0,0 +1,39 @@ +/* + * 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.internal.jctools.queues; + +import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.REF_ARRAY_BASE; +import static io.rsocket.internal.jctools.util.UnsafeRefArrayAccess.REF_ELEMENT_SHIFT; + +/** This is used for method substitution in the LinkedArray classes code generation. */ +final class LinkedArrayQueueUtil { + private LinkedArrayQueueUtil() {} + + static int length(Object[] buf) { + return buf.length; + } + + /** + * This method assumes index is actually (index << 1) because lower bit is used for resize. This + * is compensated for by reducing the element shift. The computation is constant folded, so + * there's no cost. + */ + static long modifiedCalcElementOffset(long index, long mask) { + return REF_ARRAY_BASE + ((index & mask) << (REF_ELEMENT_SHIFT - 1)); + } + + static long nextArrayOffset(Object[] curr) { + return REF_ARRAY_BASE + ((long) (length(curr) - 1) << REF_ELEMENT_SHIFT); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedQueueNode.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedQueueNode.java new file mode 100644 index 000000000..6ea69e330 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/LinkedQueueNode.java @@ -0,0 +1,59 @@ +/* + * 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.internal.jctools.queues; + +import static io.rsocket.internal.jctools.util.UnsafeAccess.UNSAFE; +import static io.rsocket.internal.jctools.util.UnsafeAccess.fieldOffset; + +final class LinkedQueueNode { + private static final long NEXT_OFFSET = fieldOffset(LinkedQueueNode.class, "next"); + + private E value; + private volatile LinkedQueueNode next; + + LinkedQueueNode() { + this(null); + } + + LinkedQueueNode(E val) { + spValue(val); + } + + /** + * Gets the current value and nulls out the reference to it from this node. + * + * @return value + */ + public E getAndNullValue() { + E temp = lpValue(); + spValue(null); + return temp; + } + + public E lpValue() { + return value; + } + + public void spValue(E newValue) { + value = newValue; + } + + public void soNext(LinkedQueueNode n) { + UNSAFE.putOrderedObject(this, NEXT_OFFSET, n); + } + + public LinkedQueueNode lvNext() { + return next; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueue.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueue.java new file mode 100644 index 000000000..e0c3d0ee1 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MessagePassingQueue.java @@ -0,0 +1,67 @@ +/* + * 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.internal.jctools.queues; + +public interface MessagePassingQueue { + int UNBOUNDED_CAPACITY = -1; + + interface Supplier { + T get(); + } + + interface Consumer { + void accept(T e); + } + + interface WaitStrategy { + int idle(int idleCounter); + } + + interface ExitCondition { + + boolean keepRunning(); + } + + boolean offer(T e); + + T poll(); + + T peek(); + + int size(); + + void clear(); + + boolean isEmpty(); + + int capacity(); + + boolean relaxedOffer(T e); + + T relaxedPoll(); + + T relaxedPeek(); + + int drain(Consumer c); + + int fill(Supplier s); + + int drain(Consumer c, int limit); + + int fill(Supplier s, int limit); + + void drain(Consumer c, WaitStrategy wait, ExitCondition exit); + + void fill(Supplier s, WaitStrategy wait, ExitCondition exit); +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MpscUnboundedArrayQueue.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MpscUnboundedArrayQueue.java new file mode 100644 index 000000000..59eab33a1 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/MpscUnboundedArrayQueue.java @@ -0,0 +1,75 @@ +/* + * 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.internal.jctools.queues; + +import static io.rsocket.internal.jctools.queues.LinkedArrayQueueUtil.length; + +import io.rsocket.internal.jctools.util.PortableJvmInfo; + +/** + * An MPSC array queue which starts at initialCapacity and grows indefinitely in linked + * chunks of the initial size. The queue grows only when the current chunk is full and elements are + * not copied on resize, instead a link to the new chunk is stored in the old chunk for the consumer + * to follow.
+ * + * @param + */ +public class MpscUnboundedArrayQueue extends BaseMpscLinkedArrayQueue { + long p0, p1, p2, p3, p4, p5, p6, p7; + long p10, p11, p12, p13, p14, p15, p16, p17; + + public MpscUnboundedArrayQueue(int chunkSize) { + super(chunkSize); + } + + @Override + protected long availableInQueue(long pIndex, long cIndex) { + return Integer.MAX_VALUE; + } + + @Override + public int capacity() { + return MessagePassingQueue.UNBOUNDED_CAPACITY; + } + + @Override + public int drain(Consumer c) { + return drain(c, 4096); + } + + @Override + public int fill(Supplier s) { + long result = + 0; // result is a long because we want to have a safepoint check at regular intervals + final int capacity = 4096; + do { + final int filled = fill(s, PortableJvmInfo.RECOMENDED_OFFER_BATCH); + if (filled == 0) { + return (int) result; + } + result += filled; + } while (result <= capacity); + return (int) result; + } + + @Override + protected int getNextBufferSize(E[] buffer) { + return length(buffer); + } + + @Override + protected long getCurrentBufferCapacity(long mask) { + return mask; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/QueueProgressIndicators.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/QueueProgressIndicators.java new file mode 100644 index 000000000..6418cc947 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/QueueProgressIndicators.java @@ -0,0 +1,50 @@ +/* + * 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.internal.jctools.queues; + +/** + * This interface is provided for monitoring purposes only and is only available on queues where it + * is easy to provide it. The producer/consumer progress indicators usually correspond with the + * number of elements offered/polled, but they are not guaranteed to maintain that semantic. + * + * @author nitsanw + */ +public interface QueueProgressIndicators { + + /** + * This method has no concurrent visibility semantics. The value returned may be negative. Under + * normal circumstances 2 consecutive calls to this method can offer an idea of progress made by + * producer threads by subtracting the 2 results though in extreme cases (if producers have + * progressed by more than 2^64) this may also fail.
+ * This value will normally indicate number of elements passed into the queue, but may under some + * circumstances be a derivative of that figure. This method should not be used to derive size or + * emptiness. + * + * @return the current value of the producer progress index + */ + long currentProducerIndex(); + + /** + * This method has no concurrent visibility semantics. The value returned may be negative. Under + * normal circumstances 2 consecutive calls to this method can offer an idea of progress made by + * consumer threads by subtracting the 2 results though in extreme cases (if consumers have + * progressed by more than 2^64) this may also fail.
+ * This value will normally indicate number of elements taken out of the queue, but may under some + * circumstances be a derivative of that figure. This method should not be used to derive size or + * emptiness. + * + * @return the current value of the consumer progress index + */ + long currentConsumerIndex(); +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/SupportsIterator.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/SupportsIterator.java new file mode 100644 index 000000000..50d2a326f --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/SupportsIterator.java @@ -0,0 +1,20 @@ +/* + * 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.internal.jctools.queues; + +import io.rsocket.internal.jctools.util.InternalAPI; + +/** Tagging interface to help testing */ +@InternalAPI +public interface SupportsIterator {} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/InternalAPI.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/InternalAPI.java new file mode 100644 index 000000000..f233e9597 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/InternalAPI.java @@ -0,0 +1,28 @@ +/* + * 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.internal.jctools.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * This annotation marks classes and methods which may be public for any reason (to support better + * testing or reduce code duplication) but are not intended as public API and may change between + * releases without the change being considered a breaking API change (a major release). + */ +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR}) +@Retention(RetentionPolicy.SOURCE) +public @interface InternalAPI {} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/PortableJvmInfo.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/PortableJvmInfo.java new file mode 100644 index 000000000..2d567d60d --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/PortableJvmInfo.java @@ -0,0 +1,23 @@ +/* + * 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.internal.jctools.util; + +/** JVM Information that is standard and available on all JVMs (i.e. does not use unsafe) */ +@InternalAPI +public interface PortableJvmInfo { + int CACHE_LINE_SIZE = Integer.getInteger("jctools.cacheLineSize", 64); + int CPUs = Runtime.getRuntime().availableProcessors(); + int RECOMENDED_OFFER_BATCH = CPUs * 4; + int RECOMENDED_POLL_BATCH = CPUs * 4; +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/Pow2.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/Pow2.java new file mode 100644 index 000000000..d8c66d89e --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/Pow2.java @@ -0,0 +1,61 @@ +/* + * 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.internal.jctools.util; + +/** Power of 2 utility functions. */ +@InternalAPI +public final class Pow2 { + public static final int MAX_POW2 = 1 << 30; + + /** + * @param value from which next positive power of two will be found. + * @return the next positive power of 2, this value if it is a power of 2. Negative values are + * mapped to 1. + * @throws IllegalArgumentException is value is more than MAX_POW2 or less than 0 + */ + public static int roundToPowerOfTwo(final int value) { + if (value > MAX_POW2) { + throw new IllegalArgumentException( + "There is no larger power of 2 int for value:" + value + " since it exceeds 2^31."); + } + if (value < 0) { + throw new IllegalArgumentException("Given value:" + value + ". Expecting value >= 0."); + } + final int nextPow2 = 1 << (32 - Integer.numberOfLeadingZeros(value - 1)); + return nextPow2; + } + + /** + * @param value to be tested to see if it is a power of two. + * @return true if the value is a power of 2 otherwise false. + */ + public static boolean isPowerOfTwo(final int value) { + return (value & (value - 1)) == 0; + } + + /** + * Align a value to the next multiple up of alignment. If the value equals an alignment multiple + * then it is returned unchanged. + * + * @param value to be aligned up. + * @param alignment to be used, must be a power of 2. + * @return the value aligned to the next boundary. + */ + public static long align(final long value, final int alignment) { + if (!isPowerOfTwo(alignment)) { + throw new IllegalArgumentException("alignment must be a power of 2:" + alignment); + } + return (value + (alignment - 1)) & ~(alignment - 1); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/RangeUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/RangeUtil.java new file mode 100644 index 000000000..77a0582ca --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/RangeUtil.java @@ -0,0 +1,57 @@ +/* + * 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.internal.jctools.util; + +@InternalAPI +public final class RangeUtil { + public static long checkPositive(long n, String name) { + if (n <= 0) { + throw new IllegalArgumentException(name + ": " + n + " (expected: > 0)"); + } + + return n; + } + + public static int checkPositiveOrZero(int n, String name) { + if (n < 0) { + throw new IllegalArgumentException(name + ": " + n + " (expected: >= 0)"); + } + + return n; + } + + public static int checkLessThan(int n, int expected, String name) { + if (n >= expected) { + throw new IllegalArgumentException(name + ": " + n + " (expected: < " + expected + ')'); + } + + return n; + } + + public static int checkLessThanOrEqual(int n, long expected, String name) { + if (n > expected) { + throw new IllegalArgumentException(name + ": " + n + " (expected: <= " + expected + ')'); + } + + return n; + } + + public static int checkGreaterThanOrEqual(int n, int expected, String name) { + if (n < expected) { + throw new IllegalArgumentException(name + ": " + n + " (expected: >= " + expected + ')'); + } + + return n; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeAccess.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeAccess.java new file mode 100755 index 000000000..793e64505 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeAccess.java @@ -0,0 +1,80 @@ +/* + * 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.internal.jctools.util; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.util.concurrent.atomic.AtomicReferenceArray; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import sun.misc.Unsafe; + +/** + * Why should we resort to using Unsafe?
+ * + *

    + *
  1. To construct class fields which allow volatile/ordered/plain access: This requirement is + * covered by {@link AtomicReferenceFieldUpdater} and similar but their performance is + * arguably worse than the DIY approach (depending on JVM version) while Unsafe + * intrinsification is a far lesser challenge for JIT compilers. + *
  2. To construct flavors of {@link AtomicReferenceArray}. + *
  3. Other use cases exist but are not present in this library yet. + *
+ * + * @author nitsanw + */ +@InternalAPI +public class UnsafeAccess { + public static final boolean SUPPORTS_GET_AND_SET; + public static final Unsafe UNSAFE; + + static { + Unsafe instance; + try { + final Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + instance = (Unsafe) field.get(null); + } catch (Exception ignored) { + // Some platforms, notably Android, might not have a sun.misc.Unsafe + // implementation with a private `theUnsafe` static instance. In this + // case we can try and call the default constructor, which proves + // sufficient for Android usage. + try { + Constructor c = Unsafe.class.getDeclaredConstructor(); + c.setAccessible(true); + instance = c.newInstance(); + } catch (Exception e) { + SUPPORTS_GET_AND_SET = false; + throw new RuntimeException(e); + } + } + + boolean getAndSetSupport = false; + try { + Unsafe.class.getMethod("getAndSetObject", Object.class, Long.TYPE, Object.class); + getAndSetSupport = true; + } catch (Exception ignored) { + } + + UNSAFE = instance; + SUPPORTS_GET_AND_SET = getAndSetSupport; + } + + public static long fieldOffset(Class clz, String fieldName) throws RuntimeException { + try { + return UNSAFE.objectFieldOffset(clz.getDeclaredField(fieldName)); + } catch (NoSuchFieldException e) { + throw new RuntimeException(e); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeRefArrayAccess.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeRefArrayAccess.java new file mode 100644 index 000000000..d8309c5c5 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/jctools/util/UnsafeRefArrayAccess.java @@ -0,0 +1,103 @@ +/* + * 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.internal.jctools.util; + +import static io.rsocket.internal.jctools.util.UnsafeAccess.UNSAFE; + +/** + * A concurrent access enabling class used by circular array based queues this class exposes an + * offset computation method along with differently memory fenced load/store methods into the + * underlying array. The class is pre-padded and the array is padded on either side to help with + * False sharing prvention. It is expected theat subclasses handle post padding. + * + *

Offset calculation is separate from access to enable the reuse of a give compute offset. + * + *

Load/Store methods using a buffer parameter are provided to allow the prevention of + * final field reload after a LoadLoad barrier. + * + *

+ * + * @author nitsanw + */ +@InternalAPI +public final class UnsafeRefArrayAccess { + public static final long REF_ARRAY_BASE; + public static final int REF_ELEMENT_SHIFT; + + static { + final int scale = UnsafeAccess.UNSAFE.arrayIndexScale(Object[].class); + if (4 == scale) { + REF_ELEMENT_SHIFT = 2; + } else if (8 == scale) { + REF_ELEMENT_SHIFT = 3; + } else { + throw new IllegalStateException("Unknown pointer size: " + scale); + } + REF_ARRAY_BASE = UnsafeAccess.UNSAFE.arrayBaseOffset(Object[].class); + } + + /** + * A plain store (no ordering/fences) of an element to a given offset + * + * @param buffer this.buffer + * @param offset computed via {@link UnsafeRefArrayAccess#calcElementOffset(long)} + * @param e an orderly kitty + */ + public static void spElement(E[] buffer, long offset, E e) { + UNSAFE.putObject(buffer, offset, e); + } + + /** + * An ordered store(store + StoreStore barrier) of an element to a given offset + * + * @param buffer this.buffer + * @param offset computed via {@link UnsafeRefArrayAccess#calcElementOffset} + * @param e an orderly kitty + */ + public static void soElement(E[] buffer, long offset, E e) { + UNSAFE.putOrderedObject(buffer, offset, e); + } + + /** + * A plain load (no ordering/fences) of an element from a given offset. + * + * @param buffer this.buffer + * @param offset computed via {@link UnsafeRefArrayAccess#calcElementOffset(long)} + * @return the element at the offset + */ + @SuppressWarnings("unchecked") + public static E lpElement(E[] buffer, long offset) { + return (E) UNSAFE.getObject(buffer, offset); + } + + /** + * A volatile load (load + LoadLoad barrier) of an element from a given offset. + * + * @param buffer this.buffer + * @param offset computed via {@link UnsafeRefArrayAccess#calcElementOffset(long)} + * @return the element at the offset + */ + @SuppressWarnings("unchecked") + public static E lvElement(E[] buffer, long offset) { + return (E) UNSAFE.getObjectVolatile(buffer, offset); + } + + /** + * @param index desirable element index + * @return the offset in bytes within the array for a given index. + */ + public static long calcElementOffset(long index) { + return REF_ARRAY_BASE + (index << REF_ELEMENT_SHIFT); + } +} diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java index 40db8ef74..990acddfe 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java @@ -20,14 +20,13 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; +import io.rsocket.internal.UnboundedProcessor; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.local.LocalServerTransport.ServerDuplexConnectionAcceptor; import java.util.Objects; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; -import reactor.core.publisher.UnicastProcessor; -import reactor.util.concurrent.Queues; /** * An implementation of {@link ClientTransport} that connects to a {@link ServerTransport} in the @@ -62,10 +61,8 @@ private Mono connect() { return Mono.error(new IllegalArgumentException("Could not find server: " + name)); } - UnicastProcessor in = - UnicastProcessor.create(Queues.unboundedMultiproducer().get()); - UnicastProcessor out = - UnicastProcessor.create(Queues.unboundedMultiproducer().get()); + UnboundedProcessor in = new UnboundedProcessor<>(); + UnboundedProcessor out = new UnboundedProcessor<>(); MonoProcessor closeNotifier = MonoProcessor.create(); server.accept(new LocalDuplexConnection(out, in, closeNotifier)); From 21afc47e1a220f11b9797e3064d789534986b528 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Wed, 10 Jul 2019 16:14:02 -0700 Subject: [PATCH 067/181] change version Signed-off-by: Robert Roeser --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c7ab3683b..9db43f0ab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-RC1-SNAPSHOT +version=1.0.0-RC1 From 42eaddea73e92a23f20846275fe4687d87cd3090 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Wed, 10 Jul 2019 16:16:28 -0700 Subject: [PATCH 068/181] change version Signed-off-by: Robert Roeser --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9db43f0ab..40d676374 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-RC1 +version=1.0.0-RC2-SNAPSHOT From 72cf960cd2b5c8e52375aa9e4dae72de640dc08a Mon Sep 17 00:00:00 2001 From: gzlicanyi Date: Wed, 24 Jul 2019 16:19:05 +0800 Subject: [PATCH 069/181] polishing (#662) * Polishing Signed-off-by: gzlicanyi * Polishing Signed-off-by: gzlicanyi --- .../java/io/rsocket/fragmentation/FrameReassembler.java | 3 +++ .../io/rsocket/internal/SynchronizedIntObjectHashMap.java | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java index 5660e3615..0c446a7c4 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java @@ -38,6 +38,9 @@ * and Reassembly */ final class FrameReassembler extends AtomicBoolean implements Disposable { + + private static final long serialVersionUID = -4394598098863449055L; + private static final Logger logger = LoggerFactory.getLogger(FrameReassembler.class); final IntObjectMap headers; diff --git a/rsocket-core/src/main/java/io/rsocket/internal/SynchronizedIntObjectHashMap.java b/rsocket-core/src/main/java/io/rsocket/internal/SynchronizedIntObjectHashMap.java index 71a4cddaa..fd6bf0aed 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/SynchronizedIntObjectHashMap.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/SynchronizedIntObjectHashMap.java @@ -189,7 +189,7 @@ public synchronized boolean isEmpty() { @Override public synchronized void clear() { - Arrays.fill(keys, (int) 0); + Arrays.fill(keys, 0); Arrays.fill(values, null); size = 0; } @@ -331,7 +331,7 @@ public synchronized Set> entrySet() { } private int objectToKey(Object key) { - return (int) ((Integer) key).intValue(); + return ((Integer) key).intValue(); } /** @@ -369,7 +369,7 @@ private int hashIndex(int key) { /** Returns the hash code for the key. */ private static int hashCode(int key) { - return (int) key; + return key; } /** Get the next sequential index after {@code index} and wraps if necessary. */ From 44f911bae4c153b77ffb9106a53cf1dae74a5738 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Wed, 24 Jul 2019 12:15:45 -0700 Subject: [PATCH 070/181] stream id wrap fix (#667) * wraps the stream id when it reaches 2^31 - it also checks for active streams and increams the id if it finds a collision Signed-off-by: Robert Roeser * fix wrapping when stream id is 0 Signed-off-by: Robert Roeser --- .../java/io/rsocket/StreamIdSupplierPerf.java | 35 ++++++++++++ .../java/io/rsocket/RSocketRequester.java | 8 +-- .../java/io/rsocket/StreamIdSupplier.java | 22 +++++--- .../java/io/rsocket/StreamIdSupplierTest.java | 54 +++++++++++++++---- 4 files changed, 97 insertions(+), 22 deletions(-) create mode 100644 rsocket-core/src/jmh/java/io/rsocket/StreamIdSupplierPerf.java diff --git a/rsocket-core/src/jmh/java/io/rsocket/StreamIdSupplierPerf.java b/rsocket-core/src/jmh/java/io/rsocket/StreamIdSupplierPerf.java new file mode 100644 index 000000000..c198b7a19 --- /dev/null +++ b/rsocket-core/src/jmh/java/io/rsocket/StreamIdSupplierPerf.java @@ -0,0 +1,35 @@ +package io.rsocket; + +import io.netty.util.collection.IntObjectMap; +import io.rsocket.internal.SynchronizedIntObjectHashMap; +import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.infra.Blackhole; + +@BenchmarkMode(Mode.Throughput) +@Fork( + value = 1 // , jvmArgsAppend = {"-Dio.netty.leakDetection.level=advanced"} + ) +@Warmup(iterations = 10) +@Measurement(iterations = 10) +@State(Scope.Thread) +public class StreamIdSupplierPerf { + @Benchmark + public void benchmarkStreamId(Input input) { + int i = input.supplier.nextStreamId(input.map); + input.bh.consume(i); + } + + @State(Scope.Benchmark) + public static class Input { + Blackhole bh; + IntObjectMap map; + StreamIdSupplier supplier; + + @Setup + public void setup(Blackhole bh) { + this.supplier = StreamIdSupplier.clientSupplier(); + this.bh = bh; + this.map = new SynchronizedIntObjectHashMap(); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java index 697d68f4a..ec4c8cec2 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java @@ -205,7 +205,7 @@ private Mono handleFireAndForget(Payload payload) { return Mono.error(err); } - final int streamId = streamIdSupplier.nextStreamId(); + final int streamId = streamIdSupplier.nextStreamId(receivers); return emptyUnicastMono() .doOnSubscribe( @@ -233,7 +233,7 @@ private Mono handleRequestResponse(final Payload payload) { return Mono.error(err); } - int streamId = streamIdSupplier.nextStreamId(); + int streamId = streamIdSupplier.nextStreamId(receivers); final UnboundedProcessor sendProcessor = this.sendProcessor; UnicastMonoProcessor receiver = UnicastMonoProcessor.create(); @@ -274,7 +274,7 @@ private Flux handleRequestStream(final Payload payload) { return Flux.error(err); } - int streamId = streamIdSupplier.nextStreamId(); + int streamId = streamIdSupplier.nextStreamId(receivers); final UnboundedProcessor sendProcessor = this.sendProcessor; final UnicastProcessor receiver = UnicastProcessor.create(); @@ -328,7 +328,7 @@ private Flux handleChannel(Flux request) { final UnboundedProcessor sendProcessor = this.sendProcessor; final UnicastProcessor receiver = UnicastProcessor.create(); - final int streamId = streamIdSupplier.nextStreamId(); + final int streamId = streamIdSupplier.nextStreamId(receivers); return receiver .doOnRequest( diff --git a/rsocket-core/src/main/java/io/rsocket/StreamIdSupplier.java b/rsocket-core/src/main/java/io/rsocket/StreamIdSupplier.java index f9985b4ac..af8c6b3d0 100644 --- a/rsocket-core/src/main/java/io/rsocket/StreamIdSupplier.java +++ b/rsocket-core/src/main/java/io/rsocket/StreamIdSupplier.java @@ -13,18 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.rsocket; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import io.netty.util.collection.IntObjectMap; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; final class StreamIdSupplier { + private static final int MASK = 0x7FFFFFFF; - private static final AtomicIntegerFieldUpdater STREAM_ID = - AtomicIntegerFieldUpdater.newUpdater(StreamIdSupplier.class, "streamId"); - private volatile int streamId; + private static final AtomicLongFieldUpdater STREAM_ID = + AtomicLongFieldUpdater.newUpdater(StreamIdSupplier.class, "streamId"); + private volatile long streamId; - private StreamIdSupplier(int streamId) { + // Visible for testing + StreamIdSupplier(int streamId) { this.streamId = streamId; } @@ -36,8 +38,12 @@ static StreamIdSupplier serverSupplier() { return new StreamIdSupplier(0); } - int nextStreamId() { - return STREAM_ID.addAndGet(this, 2); + int nextStreamId(IntObjectMap streamIds) { + int streamId; + do { + streamId = (int) STREAM_ID.addAndGet(this, 2) & MASK; + } while (streamId == 0 || streamIds.containsKey(streamId)); + return streamId; } boolean isBeforeOrCurrent(int streamId) { diff --git a/rsocket-core/src/test/java/io/rsocket/StreamIdSupplierTest.java b/rsocket-core/src/test/java/io/rsocket/StreamIdSupplierTest.java index 008e0f45a..766a6aaf7 100644 --- a/rsocket-core/src/test/java/io/rsocket/StreamIdSupplierTest.java +++ b/rsocket-core/src/test/java/io/rsocket/StreamIdSupplierTest.java @@ -20,37 +20,42 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import io.netty.util.collection.IntObjectMap; +import io.rsocket.internal.SynchronizedIntObjectHashMap; import org.junit.Test; public class StreamIdSupplierTest { @Test public void testClientSequence() { + IntObjectMap map = new SynchronizedIntObjectHashMap<>(); StreamIdSupplier s = StreamIdSupplier.clientSupplier(); - assertEquals(1, s.nextStreamId()); - assertEquals(3, s.nextStreamId()); - assertEquals(5, s.nextStreamId()); + assertEquals(1, s.nextStreamId(map)); + assertEquals(3, s.nextStreamId(map)); + assertEquals(5, s.nextStreamId(map)); } @Test public void testServerSequence() { + IntObjectMap map = new SynchronizedIntObjectHashMap<>(); StreamIdSupplier s = StreamIdSupplier.serverSupplier(); - assertEquals(2, s.nextStreamId()); - assertEquals(4, s.nextStreamId()); - assertEquals(6, s.nextStreamId()); + assertEquals(2, s.nextStreamId(map)); + assertEquals(4, s.nextStreamId(map)); + assertEquals(6, s.nextStreamId(map)); } @Test public void testClientIsValid() { + IntObjectMap map = new SynchronizedIntObjectHashMap<>(); StreamIdSupplier s = StreamIdSupplier.clientSupplier(); assertFalse(s.isBeforeOrCurrent(1)); assertFalse(s.isBeforeOrCurrent(3)); - s.nextStreamId(); + s.nextStreamId(map); assertTrue(s.isBeforeOrCurrent(1)); assertFalse(s.isBeforeOrCurrent(3)); - s.nextStreamId(); + s.nextStreamId(map); assertTrue(s.isBeforeOrCurrent(3)); // negative @@ -63,16 +68,17 @@ public void testClientIsValid() { @Test public void testServerIsValid() { + IntObjectMap map = new SynchronizedIntObjectHashMap<>(); StreamIdSupplier s = StreamIdSupplier.serverSupplier(); assertFalse(s.isBeforeOrCurrent(2)); assertFalse(s.isBeforeOrCurrent(4)); - s.nextStreamId(); + s.nextStreamId(map); assertTrue(s.isBeforeOrCurrent(2)); assertFalse(s.isBeforeOrCurrent(4)); - s.nextStreamId(); + s.nextStreamId(map); assertTrue(s.isBeforeOrCurrent(4)); // negative @@ -82,4 +88,32 @@ public void testServerIsValid() { // client also accepted (checked externally) assertTrue(s.isBeforeOrCurrent(1)); } + + @Test + public void testWrap() { + IntObjectMap map = new SynchronizedIntObjectHashMap<>(); + StreamIdSupplier s = new StreamIdSupplier(Integer.MAX_VALUE - 3); + + assertEquals(2147483646, s.nextStreamId(map)); + assertEquals(2, s.nextStreamId(map)); + assertEquals(4, s.nextStreamId(map)); + + s = new StreamIdSupplier(Integer.MAX_VALUE - 2); + + assertEquals(2147483647, s.nextStreamId(map)); + assertEquals(1, s.nextStreamId(map)); + assertEquals(3, s.nextStreamId(map)); + } + + @Test + public void testSkipFound() { + IntObjectMap map = new SynchronizedIntObjectHashMap<>(); + map.put(5, new Object()); + map.put(9, new Object()); + StreamIdSupplier s = StreamIdSupplier.clientSupplier(); + assertEquals(1, s.nextStreamId(map)); + assertEquals(3, s.nextStreamId(map)); + assertEquals(7, s.nextStreamId(map)); + assertEquals(11, s.nextStreamId(map)); + } } From 9fa00a6593e8076c5d077bc36c1c8d0fad47b21b Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Wed, 24 Jul 2019 12:15:59 -0700 Subject: [PATCH 071/181] fixes array copy in Tuple3ByteBuf (#669) * fixes array copying offsets for tuple3 bytebuf Signed-off-by: Robert Roeser * fixes array copying offsets for tuple3 bytebuf Signed-off-by: Robert Roeser * update array indexes in copy Signed-off-by: Robert Roeser --- .../java/io/rsocket/buffer/Tuple3ByteBuf.java | 5 +- .../io/rsocket/buffer/Tuple3ByteBufTest.java | 61 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java index 66fc867b0..151675746 100644 --- a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java +++ b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java @@ -144,7 +144,8 @@ public ByteBuffer[] _nioBuffers(int index, int length) { new ByteBuffer[oneBuffer.length + twoBuffer.length + threeBuffer.length]; System.arraycopy(oneBuffer, 0, results, 0, oneBuffer.length); System.arraycopy(twoBuffer, 0, results, oneBuffer.length, twoBuffer.length); - System.arraycopy(threeBuffer, 0, results, twoBuffer.length, threeBuffer.length); + System.arraycopy( + threeBuffer, 0, results, oneBuffer.length + twoBuffer.length, threeBuffer.length); return results; } else { ByteBuffer[] results = new ByteBuffer[oneBuffer.length + twoBuffer.length]; @@ -167,7 +168,7 @@ public ByteBuffer[] _nioBuffers(int index, int length) { threeBuffer = three.nioBuffers(threeReadIndex, length); ByteBuffer[] results = new ByteBuffer[twoBuffer.length + threeBuffer.length]; System.arraycopy(twoBuffer, 0, results, 0, twoBuffer.length); - System.arraycopy(threeBuffer, 0, results, threeBuffer.length, twoBuffer.length); + System.arraycopy(threeBuffer, 0, results, twoBuffer.length, threeBuffer.length); return results; } else { return twoBuffer; diff --git a/rsocket-core/src/test/java/io/rsocket/buffer/Tuple3ByteBufTest.java b/rsocket-core/src/test/java/io/rsocket/buffer/Tuple3ByteBufTest.java index 7eda472d5..4515fb29b 100644 --- a/rsocket-core/src/test/java/io/rsocket/buffer/Tuple3ByteBufTest.java +++ b/rsocket-core/src/test/java/io/rsocket/buffer/Tuple3ByteBufTest.java @@ -2,8 +2,12 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.util.concurrent.ThreadLocalRandom; +import org.junit.Assert; import org.junit.jupiter.api.Test; class Tuple3ByteBufTest { @@ -34,4 +38,61 @@ void testTupleBufferGet() { int medium = tuple.getMedium(8); } + + @Test + void testTuple3BufferSlicing() { + ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + ByteBuf one = allocator.directBuffer(); + ByteBufUtil.writeUtf8(one, "foo"); + + ByteBuf two = allocator.directBuffer(); + ByteBufUtil.writeUtf8(two, "bar"); + + ByteBuf three = allocator.directBuffer(); + ByteBufUtil.writeUtf8(three, "bar"); + + ByteBuf buf = TupleByteBuf.of(one, two, three); + + String s = buf.slice(0, 6).toString(Charset.defaultCharset()); + Assert.assertEquals("foobar", s); + + String s1 = buf.slice(3, 6).toString(Charset.defaultCharset()); + Assert.assertEquals("barbar", s1); + + String s2 = buf.slice(4, 4).toString(Charset.defaultCharset()); + Assert.assertEquals("arba", s2); + } + + @Test + void testTuple3ToNioBuffers() throws Exception { + ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + ByteBuf one = allocator.directBuffer(); + ByteBufUtil.writeUtf8(one, "one"); + + ByteBuf two = allocator.directBuffer(); + ByteBufUtil.writeUtf8(two, "two"); + + ByteBuf three = allocator.directBuffer(); + ByteBufUtil.writeUtf8(three, "three"); + + ByteBuf buf = TupleByteBuf.of(one, two, three); + ByteBuffer[] byteBuffers = buf.nioBuffers(); + + Assert.assertEquals(3, byteBuffers.length); + + ByteBuffer bb = byteBuffers[0]; + byte[] dst = new byte[bb.remaining()]; + bb.get(dst); + Assert.assertEquals("one", new String(dst, "UTF-8")); + + bb = byteBuffers[1]; + dst = new byte[bb.remaining()]; + bb.get(dst); + Assert.assertEquals("two", new String(dst, "UTF-8")); + + bb = byteBuffers[2]; + dst = new byte[bb.remaining()]; + bb.get(dst); + Assert.assertEquals("three", new String(dst, "UTF-8")); + } } From c49cbd7c482086719d8c6c3b2eb409d3eaea9d62 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 25 Jul 2019 20:57:26 +0100 Subject: [PATCH 072/181] Upgrade to Reactor Dysprosium (#671) Closes gh-670 Signed-off-by: Rossen Stoyanchev --- build.gradle | 5 +++-- .../io/rsocket/internal/UnicastMonoProcessor.java | 13 +------------ 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/build.gradle b/build.gradle index 7a6cfcebb..b4537aa1a 100644 --- a/build.gradle +++ b/build.gradle @@ -29,10 +29,10 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = 'Californium-SR8' + ext['reactor-bom.version'] = 'Dysprosium-M2' ext['logback.version'] = '1.2.3' ext['findbugs.version'] = '3.0.2' - ext['netty.version'] = '4.1.36.Final' + ext['netty.version'] = '4.1.37.Final' ext['netty-boringssl.version'] = '2.0.25.Final' ext['hdrhistogram.version'] = '2.1.10' ext['mockito.version'] = '2.25.1' @@ -89,6 +89,7 @@ subprojects { repositories { mavenCentral() + maven { url 'http://repo.spring.io/milestone' } // temporary for Reactor Dysprosium if (version.endsWith('BUILD-SNAPSHOT') || project.hasProperty('platformVersion')) { maven { url 'http://repo.spring.io/libs-snapshot' } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java index 1e616b427..35d4906ec 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java @@ -2,7 +2,6 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.function.LongSupplier; import java.util.stream.Stream; import org.reactivestreams.Processor; import org.reactivestreams.Subscription; @@ -17,12 +16,7 @@ import reactor.util.function.Tuple2; public class UnicastMonoProcessor extends Mono - implements Processor, - CoreSubscriber, - Disposable, - Subscription, - Scannable, - LongSupplier { + implements Processor, CoreSubscriber, Disposable, Subscription, Scannable { @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater ONCE = @@ -87,11 +81,6 @@ public Stream> tags() { return processor.tags(); } - @Override - public long getAsLong() { - return processor.getAsLong(); - } - @Override public void onSubscribe(Subscription s) { processor.onSubscribe(s); From 07248bbe4c9cb3e7f073772ac42460743dd0f21d Mon Sep 17 00:00:00 2001 From: Jacky Chan Date: Mon, 29 Jul 2019 16:23:34 -0700 Subject: [PATCH 073/181] Add APPLICATION_HESSIAN, APPLICATION_JAVA_OBJECT, APPLICATION_CLOUDEVENTS_JSON types (#673) Signed-off-by: linux_china --- .../src/main/java/io/rsocket/metadata/WellKnownMimeType.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java index 9ecaf0859..21c2daf93 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java @@ -67,6 +67,9 @@ public enum WellKnownMimeType { VIDEO_H264("video/H264", (byte) 0x23), VIDEO_H265("video/H265", (byte) 0x24), VIDEO_VP8("video/VP8", (byte) 0x25), + APPLICATION_HESSIAN("application/x-hessian", (byte) 0x26), + APPLICATION_JAVA_OBJECT("application/x-java-object", (byte) 0x27), + APPLICATION_CLOUDEVENTS_JSON("application/cloudevents+json", (byte) 0x28), // ... reserved for future use ... From 6c39ad39678176b2c543a9aba1d174bfbf8e6f61 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 31 Jul 2019 04:20:56 +0100 Subject: [PATCH 074/181] Upgrade to Dysprosium M3 (#674) Signed-off-by: Rossen Stoyanchev --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b4537aa1a..3ce473f9c 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = 'Dysprosium-M2' + ext['reactor-bom.version'] = 'Dysprosium-M3' ext['logback.version'] = '1.2.3' ext['findbugs.version'] = '3.0.2' ext['netty.version'] = '4.1.37.Final' From 27feee63ebf9be34f77226f35e0905f183f793bf Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 1 Aug 2019 13:54:08 +0300 Subject: [PATCH 075/181] ignores temporary WebSocketClient tests Signed-off-by: Oleh Dokuka --- .travis.yml | 2 ++ .../transport/netty/client/WebsocketClientTransportTest.java | 4 ++++ 2 files changed, 6 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2a4ab71da..c1e802935 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,8 @@ --- language: java +dist: trusty + matrix: include: - jdk: oraclejdk8 diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java index 9d989d769..905f022f2 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java @@ -25,6 +25,7 @@ import java.net.URI; import java.util.Collections; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -39,6 +40,7 @@ final class WebsocketClientTransportTest { @Test + @Disabled public void testThatSetupWithUnSpecifiedFrameSizeShouldSetMaxFrameSize() { ArgumentCaptor captor = ArgumentCaptor.forClass(Integer.class); HttpClient httpClient = Mockito.spy(HttpClient.create()); @@ -53,6 +55,7 @@ public void testThatSetupWithUnSpecifiedFrameSizeShouldSetMaxFrameSize() { } @Test + @Disabled public void testThatSetupWithSpecifiedFrameSizeButLowerThanWsDefaultShouldSetToWsDefault() { ArgumentCaptor captor = ArgumentCaptor.forClass(Integer.class); HttpClient httpClient = Mockito.spy(HttpClient.create()); @@ -67,6 +70,7 @@ public void testThatSetupWithSpecifiedFrameSizeButLowerThanWsDefaultShouldSetToW } @Test + @Disabled public void testThatSetupWithSpecifiedFrameSizeButHigherThanWsDefaultShouldSetToSpecifiedFrameSize() { ArgumentCaptor captor = ArgumentCaptor.forClass(Integer.class); From cf1b5d65d122cfaef55417acfe54c2af94a20982 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Tue, 30 Jul 2019 17:05:54 +0300 Subject: [PATCH 076/181] Setup payload: distinguish missing and empty metadata Signed-off-by: Maksym Ostroverkhov --- .../main/java/io/rsocket/RSocketFactory.java | 3 +- .../io/rsocket/frame/SetupFrameFlyweight.java | 17 ++-- .../rsocket/ConnectionSetupPayloadTest.java | 88 +++++++++++++++++++ .../java/io/rsocket/RSocketLeaseTest.java | 3 +- .../java/io/rsocket/SetupRejectionTest.java | 9 +- .../frame/SetupFrameFlyweightTest.java | 16 ++-- .../ClientServerInputMultiplexerTest.java | 4 +- .../main/java/io/rsocket/test/TestFrames.java | 4 +- 8 files changed, 110 insertions(+), 34 deletions(-) create mode 100644 rsocket-core/src/test/java/io/rsocket/ConnectionSetupPayloadTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 5e152fbc7..8a3e3ae25 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -355,8 +355,7 @@ public Mono start() { resumeToken, metadataMimeType, dataMimeType, - setupPayload.sliceMetadata(), - setupPayload.sliceData()); + setupPayload); RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java index 440b9e178..9f92e715f 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java @@ -4,6 +4,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; +import io.rsocket.Payload; import java.nio.charset.StandardCharsets; public class SetupFrameFlyweight { @@ -27,13 +28,12 @@ public class SetupFrameFlyweight { public static ByteBuf encode( final ByteBufAllocator allocator, - boolean lease, + final boolean lease, final int keepaliveInterval, final int maxLifetime, final String metadataMimeType, final String dataMimeType, - final ByteBuf metadata, - final ByteBuf data) { + final Payload setupPayload) { return encode( allocator, lease, @@ -42,20 +42,21 @@ public static ByteBuf encode( Unpooled.EMPTY_BUFFER, metadataMimeType, dataMimeType, - metadata, - data); + setupPayload); } public static ByteBuf encode( final ByteBufAllocator allocator, - boolean lease, + final boolean lease, final int keepaliveInterval, final int maxLifetime, final ByteBuf resumeToken, final String metadataMimeType, final String dataMimeType, - final ByteBuf metadata, - final ByteBuf data) { + final Payload setupPayload) { + + ByteBuf metadata = setupPayload.hasMetadata() ? setupPayload.sliceMetadata() : null; + ByteBuf data = setupPayload.sliceData(); int flags = 0; diff --git a/rsocket-core/src/test/java/io/rsocket/ConnectionSetupPayloadTest.java b/rsocket-core/src/test/java/io/rsocket/ConnectionSetupPayloadTest.java new file mode 100644 index 000000000..16e0f2ec7 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/ConnectionSetupPayloadTest.java @@ -0,0 +1,88 @@ +package io.rsocket; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.util.DefaultPayload; +import org.junit.jupiter.api.Test; + +class ConnectionSetupPayloadTest { + private static final int KEEP_ALIVE_INTERVAL = 5; + private static final int KEEP_ALIVE_MAX_LIFETIME = 500; + private static final String METADATA_TYPE = "metadata_type"; + private static final String DATA_TYPE = "data_type"; + + @Test + void testSetupPayloadWithDataMetadata() { + ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); + ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {2, 1, 0}); + Payload payload = DefaultPayload.create(data, metadata); + boolean leaseEnabled = true; + + ByteBuf frame = encodeSetupFrame(leaseEnabled, payload); + ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(frame); + + assertTrue(setupPayload.willClientHonorLease()); + assertEquals(KEEP_ALIVE_INTERVAL, setupPayload.keepAliveInterval()); + assertEquals(KEEP_ALIVE_MAX_LIFETIME, setupPayload.keepAliveMaxLifetime()); + assertEquals(METADATA_TYPE, SetupFrameFlyweight.metadataMimeType(frame)); + assertEquals(DATA_TYPE, SetupFrameFlyweight.dataMimeType(frame)); + assertTrue(setupPayload.hasMetadata()); + assertNotNull(setupPayload.metadata()); + assertEquals(payload.metadata(), setupPayload.metadata()); + assertEquals(payload.data(), setupPayload.data()); + frame.release(); + } + + @Test + void testSetupPayloadWithNoMetadata() { + ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); + ByteBuf metadata = null; + Payload payload = DefaultPayload.create(data, metadata); + boolean leaseEnabled = false; + + ByteBuf frame = encodeSetupFrame(leaseEnabled, payload); + ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(frame); + + assertFalse(setupPayload.willClientHonorLease()); + assertFalse(setupPayload.hasMetadata()); + assertNotNull(setupPayload.metadata()); + assertEquals(0, setupPayload.metadata().readableBytes()); + assertEquals(payload.data(), setupPayload.data()); + frame.release(); + } + + @Test + void testSetupPayloadWithEmptyMetadata() { + ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); + ByteBuf metadata = Unpooled.EMPTY_BUFFER; + Payload payload = DefaultPayload.create(data, metadata); + boolean leaseEnabled = false; + + ByteBuf frame = encodeSetupFrame(leaseEnabled, payload); + ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(frame); + + assertFalse(setupPayload.willClientHonorLease()); + assertTrue(setupPayload.hasMetadata()); + assertNotNull(setupPayload.metadata()); + assertEquals(0, setupPayload.metadata().readableBytes()); + assertEquals(payload.data(), setupPayload.data()); + frame.release(); + } + + private static ByteBuf encodeSetupFrame(boolean leaseEnabled, Payload setupPayload) { + return SetupFrameFlyweight.encode( + ByteBufAllocator.DEFAULT, + leaseEnabled, + KEEP_ALIVE_INTERVAL, + KEEP_ALIVE_MAX_LIFETIME, + null, + METADATA_TYPE, + DATA_TYPE, + setupPayload); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java index 368a69b9e..2a2567843 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java @@ -124,8 +124,7 @@ public void serverRSocketFactoryRejectsUnsupportedLease() { 30_000, "application/octet-stream", "application/octet-stream", - payload.sliceMetadata(), - payload.sliceData()); + payload); TestServerTransport transport = new TestServerTransport(); Closeable server = diff --git a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java index 20eb9f5a2..75e9f5a85 100644 --- a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java @@ -149,14 +149,7 @@ public void connect() { Payload payload = DefaultPayload.create(DefaultPayload.EMPTY_BUFFER); ByteBuf setup = SetupFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, - false, - 0, - 42, - "mdMime", - "dMime", - payload.sliceMetadata(), - payload.sliceData()); + ByteBufAllocator.DEFAULT, false, 0, 42, "mdMime", "dMime", payload); conn.addToReceivedBuffer(setup); } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java index 73527536f..128b3ff84 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java @@ -5,6 +5,8 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.rsocket.Payload; +import io.rsocket.util.DefaultPayload; import java.util.Arrays; import org.junit.jupiter.api.Test; @@ -13,9 +15,10 @@ class SetupFrameFlyweightTest { void testEncodingNoResume() { ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); + Payload payload = DefaultPayload.create(data, metadata); ByteBuf frame = SetupFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, false, 5, 500, "metadata_type", "data_type", metadata, data); + ByteBufAllocator.DEFAULT, false, 5, 500, "metadata_type", "data_type", payload); assertEquals(FrameType.SETUP, FrameHeaderFlyweight.frameType(frame)); assertFalse(SetupFrameFlyweight.resumeEnabled(frame)); @@ -34,18 +37,11 @@ void testEncodingResume() { Arrays.fill(tokenBytes, (byte) 1); ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); + Payload payload = DefaultPayload.create(data, metadata); ByteBuf token = Unpooled.wrappedBuffer(tokenBytes); ByteBuf frame = SetupFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, - true, - 5, - 500, - token, - "metadata_type", - "data_type", - metadata, - data); + ByteBufAllocator.DEFAULT, true, 5, 500, token, "metadata_type", "data_type", payload); assertEquals(FrameType.SETUP, FrameHeaderFlyweight.frameType(frame)); assertTrue(SetupFrameFlyweight.honorLease(frame)); diff --git a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java index 9efe66a16..8f56608d8 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java @@ -24,6 +24,7 @@ import io.rsocket.frame.*; import io.rsocket.plugins.PluginRegistry; import io.rsocket.test.util.TestDuplexConnection; +import io.rsocket.util.DefaultPayload; import java.util.concurrent.atomic.AtomicInteger; import org.junit.Before; import org.junit.Test; @@ -199,8 +200,7 @@ private ByteBuf setupFrame() { 42, "application/octet-stream", "application/octet-stream", - Unpooled.EMPTY_BUFFER, - Unpooled.EMPTY_BUFFER); + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER)); } private ByteBuf leaseFrame() { diff --git a/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java index 815a17980..2651b14ec 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java @@ -22,6 +22,7 @@ import io.rsocket.Payload; import io.rsocket.frame.*; import io.rsocket.util.DefaultPayload; +import io.rsocket.util.EmptyPayload; /** Test instances of all frame types. */ public final class TestFrames { @@ -104,7 +105,6 @@ public static ByteBuf createTestSetupFrame() { Unpooled.EMPTY_BUFFER, "metadataType", "dataType", - null, - Unpooled.EMPTY_BUFFER); + EmptyPayload.INSTANCE); } } From 871889431086479dd7f98424ab63c89aaba37668 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Thu, 1 Aug 2019 21:05:55 +0300 Subject: [PATCH 077/181] fix #665 add ConnectionSetupPayload.isResumeEnabled and resumeToken --- .../java/io/rsocket/ConnectionSetupPayload.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java index dcae3b74e..44bcaf6a1 100644 --- a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java @@ -20,6 +20,7 @@ import io.netty.util.AbstractReferenceCounted; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.SetupFrameFlyweight; +import javax.annotation.Nullable; /** * Exposed to server for determination of ResponderRSocket based on mime types and SETUP @@ -43,6 +44,11 @@ public static ConnectionSetupPayload create(final ByteBuf setupFrame) { public abstract boolean willClientHonorLease(); + public abstract boolean isResumeEnabled(); + + @Nullable + public abstract ByteBuf resumeToken(); + @Override public ConnectionSetupPayload retain() { super.retain(); @@ -101,6 +107,16 @@ public boolean willClientHonorLease() { return SetupFrameFlyweight.honorLease(setupFrame); } + @Override + public boolean isResumeEnabled() { + return SetupFrameFlyweight.resumeEnabled(setupFrame); + } + + @Override + public ByteBuf resumeToken() { + return SetupFrameFlyweight.resumeToken(setupFrame); + } + @Override public ConnectionSetupPayload touch() { setupFrame.touch(); From cce337ad7e9f726d052e5f019bb66ca2f812c3cd Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Thu, 1 Aug 2019 11:38:56 -0700 Subject: [PATCH 078/181] update version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 40d676374..c6d02bbef 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-RC2-SNAPSHOT +version=1.0.0-RC2 From 6a63b3d795d03c49192b8eab67d63a3a5c82ebeb Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Thu, 1 Aug 2019 11:40:17 -0700 Subject: [PATCH 079/181] update version Signed-off-by: Robert Roeser --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c6d02bbef..433d1c709 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-RC2 +version=1.0.0-RC3 From 2ac4f47ce12710b55435de880142a0ee5befedb8 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Fri, 2 Aug 2019 10:59:17 -0700 Subject: [PATCH 080/181] update version in readme --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 69e0e65e2..6eb8ab527 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ Example: ```groovy dependencies { - implementation 'io.rsocket:rsocket-core:0.12.2-RC4' - implementation 'io.rsocket:rsocket-transport-netty:0.12.2-RC4' -// implementation 'io.rsocket:rsocket-core:1.0.0-RC1-SNAPSHOT' -// implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC1-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:1.0.0-RC2' + implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC2' +// implementation 'io.rsocket:rsocket-core:1.0.0-RC3-SNAPSHOT' +// implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC3-SNAPSHOT' } ``` From 8969401588ea8d9c38e416903c987aca46f87967 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 5 Aug 2019 20:13:23 +0100 Subject: [PATCH 081/181] Add SocketAcceptorInterceptor (#676) * SocketAcceptor applied symmetrically This commit deprecates the client side acceptor(BiFunction) and adds support for SocketAcceptor instead. This makes it possible for a client side acceptor to be asynchronous too, and allows applying the same acceptor both client and server side. Signed-off-by: Rossen Stoyanchev * Add SocketAcceptorInteceptor This commit adds an interceptor for SocketAcceptor. This provides access to connection setup information and also allows applying requester and responder interceptors from one place. Signed-off-by: Rossen Stoyanchev --- .../main/java/io/rsocket/RSocketFactory.java | 110 +++++++++++------- .../main/java/io/rsocket/SocketAcceptor.java | 19 +-- .../io/rsocket/plugins/PluginRegistry.java | 14 +++ .../plugins/SocketAcceptorInterceptor.java | 29 +++++ .../rsocket/integration/IntegrationTest.java | 47 ++++++-- 5 files changed, 155 insertions(+), 64 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 8a3e3ae25..b6c268464 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -31,11 +31,11 @@ import io.rsocket.internal.ClientSetup; import io.rsocket.internal.ServerSetup; import io.rsocket.keepalive.KeepAliveHandler; -import io.rsocket.lease.*; -import io.rsocket.plugins.DuplexConnectionInterceptor; -import io.rsocket.plugins.PluginRegistry; -import io.rsocket.plugins.Plugins; -import io.rsocket.plugins.RSocketInterceptor; +import io.rsocket.lease.LeaseStats; +import io.rsocket.lease.Leases; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.plugins.*; import io.rsocket.resume.*; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; @@ -44,7 +44,10 @@ import io.rsocket.util.MultiSubscriberRSocket; import java.time.Duration; import java.util.Objects; -import java.util.function.*; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; import reactor.core.publisher.Mono; /** Factory for creating RSocket clients and servers. */ @@ -93,10 +96,7 @@ default Start transport(ServerTransport transport) { public static class ClientRSocketFactory implements ClientTransportAcceptor { private static final String CLIENT_TAG = "client"; - private Supplier> acceptor = - () -> rSocket -> new AbstractRSocket() {}; - - private BiFunction biAcceptor; + private SocketAcceptor acceptor = (setup, sendingSocket) -> Mono.just(new AbstractRSocket() {}); private Consumer errorConsumer = Throwable::printStackTrace; private int mtu = 0; @@ -161,6 +161,11 @@ public ClientRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { return this; } + public ClientRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor) { + plugins.addSocketAcceptorPlugin(interceptor); + return this; + } + /** * Deprecated as Keep-Alive is not optional according to spec * @@ -268,18 +273,25 @@ public Start transport(Supplier transportClient) { } public ClientTransportAcceptor acceptor(Function acceptor) { - this.acceptor = () -> acceptor; - return StartClient::new; + return acceptor(() -> acceptor); } public ClientTransportAcceptor acceptor(Supplier> acceptor) { - this.acceptor = acceptor; - return StartClient::new; + return acceptor( + (SocketAcceptor) + (setup, sendingSocket) -> Mono.just(acceptor.get().apply(sendingSocket))); } + @Deprecated public ClientTransportAcceptor acceptor( BiFunction biAcceptor) { - this.biAcceptor = biAcceptor; + return acceptor( + (SocketAcceptor) + (setup, sendingSocket) -> Mono.just(biAcceptor.apply(setup, sendingSocket))); + } + + public ClientTransportAcceptor acceptor(SocketAcceptor acceptor) { + this.acceptor = acceptor; return StartClient::new; } @@ -346,6 +358,8 @@ public Mono start() { rSocketRequester = new MultiSubscriberRSocket(rSocketRequester); } + RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); + ByteBuf setupFrame = SetupFrameFlyweight.encode( allocator, @@ -357,34 +371,38 @@ public Mono start() { dataMimeType, setupPayload); - RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); - - RSocket rSocketHandler; - if (biAcceptor != null) { - ConnectionSetupPayload setup = ConnectionSetupPayload.create(setupFrame); - rSocketHandler = biAcceptor.apply(setup, wrappedRSocketRequester); - } else { - rSocketHandler = acceptor.get().apply(wrappedRSocketRequester); - } - - RSocket wrappedRSocketHandler = plugins.applyResponder(rSocketHandler); - - ResponderLeaseHandler responderLeaseHandler = - isLeaseEnabled - ? new ResponderLeaseHandler.Impl<>( - CLIENT_TAG, allocator, leases.sender(), errorConsumer, leases.stats()) - : ResponderLeaseHandler.None; - - RSocket rSocketResponder = - new RSocketResponder( - allocator, - multiplexer.asServerConnection(), - wrappedRSocketHandler, - payloadDecoder, - errorConsumer, - responderLeaseHandler); + ConnectionSetupPayload setup = ConnectionSetupPayload.create(setupFrame); + + return plugins + .applySocketAcceptorInterceptor(acceptor) + .accept(setup, wrappedRSocketRequester) + .flatMap( + rSocketHandler -> { + RSocket wrappedRSocketHandler = plugins.applyResponder(rSocketHandler); + + ResponderLeaseHandler responderLeaseHandler = + isLeaseEnabled + ? new ResponderLeaseHandler.Impl<>( + CLIENT_TAG, + allocator, + leases.sender(), + errorConsumer, + leases.stats()) + : ResponderLeaseHandler.None; + + RSocket rSocketResponder = + new RSocketResponder( + allocator, + multiplexer.asServerConnection(), + wrappedRSocketHandler, + payloadDecoder, + errorConsumer, + responderLeaseHandler); - return wrappedConnection.sendOne(setupFrame).thenReturn(wrappedRSocketRequester); + return wrappedConnection + .sendOne(setupFrame) + .thenReturn(wrappedRSocketRequester); + }); }); } @@ -476,6 +494,11 @@ public ServerRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { return this; } + public ServerRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor) { + plugins.addSocketAcceptorPlugin(interceptor); + return this; + } + public ServerTransportAcceptor acceptor(SocketAcceptor acceptor) { this.acceptor = acceptor; return new ServerStart<>(); @@ -644,7 +667,8 @@ private Mono acceptSetup( } RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); - return acceptor + return plugins + .applySocketAcceptorInterceptor(acceptor) .accept(setupPayload, wrappedRSocketRequester) .onErrorResume( err -> sendError(multiplexer, rejectedSetupError(err)).then(Mono.error(err))) diff --git a/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java b/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java index 0f6b99d0e..85c731eea 100644 --- a/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java @@ -20,20 +20,21 @@ import reactor.core.publisher.Mono; /** - * {@code RSocket} is a full duplex protocol where a client and server are identical in terms of - * both having the capability to initiate requests to their peer. This interface provides the - * contract where a server accepts a new {@code RSocket} for sending requests to the peer and - * returns a new {@code RSocket} that will be used to accept requests from it's peer. + * RSocket is a full duplex protocol where a client and server are identical in terms of both having + * the capability to initiate requests to their peer. This interface provides the contract where a + * client or server handles the {@code setup} for a new connection and creates a responder {@code + * RSocket} for accepting requests from the remote peer. */ public interface SocketAcceptor { /** - * Accepts a new {@code RSocket} used to send requests to the peer and returns another {@code - * RSocket} that is used for accepting requests from the peer. + * Handle the {@code SETUP} frame for a new connection and create a responder {@code RSocket} for + * handling requests from the remote peer. * - * @param setup Setup as sent by the client. - * @param sendingSocket Socket used to send requests to the peer. - * @return Socket to accept requests from the peer. + * @param setup the {@code setup} received from a client in a server scenario, or in a client + * scenario this is the setup about to be sent to the server. + * @param sendingSocket socket for sending requests to the remote peer. + * @return {@code RSocket} to accept requests with. * @throws SetupException If the acceptor needs to reject the setup of this socket. */ Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket); diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java index 676cfc19c..e3a19367c 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java @@ -18,6 +18,7 @@ import io.rsocket.DuplexConnection; import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; import java.util.ArrayList; import java.util.List; @@ -25,6 +26,7 @@ public class PluginRegistry { private List connections = new ArrayList<>(); private List requesters = new ArrayList<>(); private List responders = new ArrayList<>(); + private List socketAcceptorInterceptors = new ArrayList<>(); public PluginRegistry() {} @@ -58,6 +60,10 @@ public void addResponderPlugin(RSocketInterceptor interceptor) { responders.add(interceptor); } + public void addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor) { + socketAcceptorInterceptors.add(interceptor); + } + /** Deprecated. Use {@link #applyRequester(RSocket)} instead */ @Deprecated public RSocket applyClient(RSocket rSocket) { @@ -86,6 +92,14 @@ public RSocket applyResponder(RSocket rSocket) { return rSocket; } + public SocketAcceptor applySocketAcceptorInterceptor(SocketAcceptor acceptor) { + for (SocketAcceptorInterceptor i : socketAcceptorInterceptors) { + acceptor = i.apply(acceptor); + } + + return acceptor; + } + public DuplexConnection applyConnection( DuplexConnectionInterceptor.Type type, DuplexConnection connection) { for (DuplexConnectionInterceptor i : connections) { diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java new file mode 100644 index 000000000..c9201ca5b --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2019 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 + * + * https://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.plugins; + +import io.rsocket.SocketAcceptor; +import java.util.function.Function; + +/** + * Contract to decorate a {@link SocketAcceptor}, providing access to connection {@code setup} + * information and the ability to also decorate the sockets for requesting and responding. + * + *

This can be used as an alternative to individual requester and responder {@link + * RSocketInterceptor} plugins. + */ +public @FunctionalInterface interface SocketAcceptorInterceptor + extends Function {} diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java index 627b1d7da..c7dfe34c6 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java @@ -29,6 +29,7 @@ import io.rsocket.RSocketFactory; import io.rsocket.plugins.DuplexConnectionInterceptor; import io.rsocket.plugins.RSocketInterceptor; +import io.rsocket.plugins.SocketAcceptorInterceptor; import io.rsocket.test.TestSubscriber; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.CloseableChannel; @@ -48,34 +49,52 @@ public class IntegrationTest { - private static final RSocketInterceptor clientPlugin; - private static final RSocketInterceptor serverPlugin; + private static final RSocketInterceptor requesterPlugin; + private static final RSocketInterceptor responderPlugin; + private static final SocketAcceptorInterceptor clientAcceptorPlugin; + private static final SocketAcceptorInterceptor serverAcceptorPlugin; private static final DuplexConnectionInterceptor connectionPlugin; - public static volatile boolean calledClient = false; - public static volatile boolean calledServer = false; + public static volatile boolean calledRequester = false; + public static volatile boolean calledResponder = false; + public static volatile boolean calledClientAcceptor = false; + public static volatile boolean calledServerAcceptor = false; public static volatile boolean calledFrame = false; static { - clientPlugin = + requesterPlugin = reactiveSocket -> new RSocketProxy(reactiveSocket) { @Override public Mono requestResponse(Payload payload) { - calledClient = true; + calledRequester = true; return reactiveSocket.requestResponse(payload); } }; - serverPlugin = + responderPlugin = reactiveSocket -> new RSocketProxy(reactiveSocket) { @Override public Mono requestResponse(Payload payload) { - calledServer = true; + calledResponder = true; return reactiveSocket.requestResponse(payload); } }; + clientAcceptorPlugin = + acceptor -> + (setup, sendingSocket) -> { + calledClientAcceptor = true; + return acceptor.accept(setup, sendingSocket); + }; + + serverAcceptorPlugin = + acceptor -> + (setup, sendingSocket) -> { + calledServerAcceptor = true; + return acceptor.accept(setup, sendingSocket); + }; + connectionPlugin = (type, connection) -> { calledFrame = true; @@ -99,7 +118,8 @@ public void startup() { server = RSocketFactory.receive() - .addServerPlugin(serverPlugin) + .addResponderPlugin(responderPlugin) + .addSocketAcceptorPlugin(serverAcceptorPlugin) .addConnectionPlugin(connectionPlugin) .errorConsumer( t -> { @@ -138,7 +158,8 @@ public Flux requestChannel(Publisher payloads) { client = RSocketFactory.connect() - .addClientPlugin(clientPlugin) + .addRequesterPlugin(requesterPlugin) + .addSocketAcceptorPlugin(clientAcceptorPlugin) .addConnectionPlugin(connectionPlugin) .transport(TcpClientTransport.create(server.address())) .start() @@ -154,8 +175,10 @@ public void teardown() { public void testRequest() { client.requestResponse(DefaultPayload.create("REQUEST", "META")).block(); assertThat("Server did not see the request.", requestCount.get(), is(1)); - assertTrue(calledClient); - assertTrue(calledServer); + assertTrue(calledRequester); + assertTrue(calledResponder); + assertTrue(calledClientAcceptor); + assertTrue(calledServerAcceptor); assertTrue(calledFrame); } From 88ee909b8bfb78b5c53fa25bc39e16f6d893f0df Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 5 Aug 2019 22:14:03 +0300 Subject: [PATCH 082/181] Feature/rate limiter publisher (#672) * partial rate limiting impl Signed-off-by: Oleh Dokuka * fixes tests Signed-off-by: Oleh Dokuka * Prototyping ratelimited request publisher replacement for coordinated request publisher Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/RSocketRequester.java | 21 +- .../java/io/rsocket/RSocketResponder.java | 9 +- .../RateLimitableRequestPublisher.java | 242 ++++++++++++++++++ .../RateLimitableRequestPublisherTest.java | 140 ++++++++++ .../test/util/TestDuplexConnection.java | 9 +- .../rsocket/integration/IntegrationTest.java | 2 +- .../integration/InteractionsLoadTest.java | 2 +- .../integration/TcpIntegrationTest.java | 2 +- .../io/rsocket/integration/FragmentTest.java | 2 +- .../transport/netty/TcpPongServer.java | 2 +- .../netty/server/TcpServerTransportTest.java | 4 +- 11 files changed, 412 insertions(+), 23 deletions(-) create mode 100755 rsocket-core/src/main/java/io/rsocket/internal/RateLimitableRequestPublisher.java create mode 100644 rsocket-core/src/test/java/io/rsocket/internal/RateLimitableRequestPublisherTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java index ec4c8cec2..f921365da 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java @@ -27,7 +27,7 @@ import io.rsocket.exceptions.Exceptions; import io.rsocket.frame.*; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.LimitableRequestPublisher; +import io.rsocket.internal.RateLimitableRequestPublisher; import io.rsocket.internal.SynchronizedIntObjectHashMap; import io.rsocket.internal.UnboundedProcessor; import io.rsocket.internal.UnicastMonoProcessor; @@ -47,6 +47,7 @@ import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.*; +import reactor.util.concurrent.Queues; /** * Requester Side of a RSocket socket. Sends {@link ByteBuf}s to a {@link RSocketResponder} of peer @@ -60,7 +61,7 @@ class RSocketRequester implements RSocket { private final PayloadDecoder payloadDecoder; private final Consumer errorConsumer; private final StreamIdSupplier streamIdSupplier; - private final IntObjectMap senders; + private final IntObjectMap senders; private final IntObjectMap> receivers; private final UnboundedProcessor sendProcessor; private final RequesterLeaseHandler leaseHandler; @@ -131,7 +132,7 @@ private void handleSendProcessorError(Throwable t) { } }); - senders.values().forEach(LimitableRequestPublisher::cancel); + senders.values().forEach(RateLimitableRequestPublisher::cancel); } private void handleSendProcessorCancel(SignalType t) { @@ -150,7 +151,7 @@ private void handleSendProcessorCancel(SignalType t) { } }); - senders.values().forEach(LimitableRequestPublisher::cancel); + senders.values().forEach(RateLimitableRequestPublisher::cancel); } @Override @@ -343,8 +344,8 @@ public void accept(long n) { request .transform( f -> { - LimitableRequestPublisher wrapped = - LimitableRequestPublisher.wrap(f); + RateLimitableRequestPublisher wrapped = + RateLimitableRequestPublisher.wrap(f, Queues.SMALL_BUFFER_SIZE); // Need to set this to one for first the frame wrapped.request(1); senders.put(streamId, wrapped); @@ -421,7 +422,7 @@ protected void hookOnError(Throwable t) { .doFinally( s -> { receivers.remove(streamId); - LimitableRequestPublisher sender = senders.remove(streamId); + RateLimitableRequestPublisher sender = senders.remove(streamId); if (sender != null) { sender.cancel(); } @@ -489,7 +490,7 @@ private void setTerminationError(Throwable error) { } private synchronized void cleanUpLimitableRequestPublisher( - LimitableRequestPublisher limitableRequestPublisher) { + RateLimitableRequestPublisher limitableRequestPublisher) { try { limitableRequestPublisher.cancel(); } catch (Throwable t) { @@ -561,7 +562,7 @@ private void handleFrame(int streamId, FrameType type, ByteBuf frame) { break; case CANCEL: { - LimitableRequestPublisher sender = senders.remove(streamId); + RateLimitableRequestPublisher sender = senders.remove(streamId); if (sender != null) { sender.cancel(); } @@ -572,7 +573,7 @@ private void handleFrame(int streamId, FrameType type, ByteBuf frame) { break; case REQUEST_N: { - LimitableRequestPublisher sender = senders.get(streamId); + RateLimitableRequestPublisher sender = senders.get(streamId); if (sender != null) { int n = RequestNFrameFlyweight.requestN(frame); sender.request(n >= Integer.MAX_VALUE ? Long.MAX_VALUE : n); diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java index 3bd221d64..490b00967 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java @@ -23,7 +23,7 @@ import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.frame.*; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.LimitableRequestPublisher; +import io.rsocket.internal.RateLimitableRequestPublisher; import io.rsocket.internal.SynchronizedIntObjectHashMap; import io.rsocket.internal.UnboundedProcessor; import io.rsocket.lease.ResponderLeaseHandler; @@ -35,6 +35,7 @@ import reactor.core.Disposable; import reactor.core.Exceptions; import reactor.core.publisher.*; +import reactor.util.concurrent.Queues; /** Responder side of RSocket. Receives {@link ByteBuf}s from a peer's {@link RSocketRequester} */ class RSocketResponder implements ResponderRSocket { @@ -46,7 +47,7 @@ class RSocketResponder implements ResponderRSocket { private final Consumer errorConsumer; private final ResponderLeaseHandler leaseHandler; - private final IntObjectMap sendingLimitableSubscriptions; + private final IntObjectMap sendingLimitableSubscriptions; private final IntObjectMap sendingSubscriptions; private final IntObjectMap> channelProcessors; @@ -435,8 +436,8 @@ private void handleStream(int streamId, Flux response, int initialReque response .transform( frameFlux -> { - LimitableRequestPublisher payloads = - LimitableRequestPublisher.wrap(frameFlux); + RateLimitableRequestPublisher payloads = + RateLimitableRequestPublisher.wrap(frameFlux, Queues.SMALL_BUFFER_SIZE); sendingLimitableSubscriptions.put(streamId, payloads); payloads.request( initialRequestN >= Integer.MAX_VALUE ? Long.MAX_VALUE : initialRequestN); diff --git a/rsocket-core/src/main/java/io/rsocket/internal/RateLimitableRequestPublisher.java b/rsocket-core/src/main/java/io/rsocket/internal/RateLimitableRequestPublisher.java new file mode 100755 index 000000000..cdb0d0c0c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/RateLimitableRequestPublisher.java @@ -0,0 +1,242 @@ +/* + * Copyright 2015-2018 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.internal; + +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import javax.annotation.Nullable; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Operators; + +/** */ +public class RateLimitableRequestPublisher extends Flux implements Subscription { + + private static final int NOT_CANCELED_STATE = 0; + private static final int CANCELED_STATE = 1; + + private final Publisher source; + + private volatile int canceled; + private static final AtomicIntegerFieldUpdater CANCELED = + AtomicIntegerFieldUpdater.newUpdater(RateLimitableRequestPublisher.class, "canceled"); + + private final long prefetch; + private final long limit; + + private long externalRequested; // need sync + private int pendingToFulfil; // need sync since should be checked/zerroed in onNext + // and increased in request + private int deliveredElements; // no need to sync since increased zerroed only in + // the request method + + private boolean subscribed; + + private @Nullable Subscription internalSubscription; + + private RateLimitableRequestPublisher(Publisher source, long prefetch) { + this.source = source; + this.prefetch = prefetch; + this.limit = prefetch == Integer.MAX_VALUE ? Integer.MAX_VALUE : (prefetch - (prefetch >> 2)); + } + + public static RateLimitableRequestPublisher wrap(Publisher source, long prefetch) { + return new RateLimitableRequestPublisher<>(source, prefetch); + } + + @Override + public void subscribe(CoreSubscriber destination) { + synchronized (this) { + if (subscribed) { + throw new IllegalStateException("only one subscriber at a time"); + } + + subscribed = true; + } + final InnerOperator s = new InnerOperator(destination); + + source.subscribe(s); + destination.onSubscribe(s); + } + + @Override + public void request(long n) { + synchronized (this) { + long requested = externalRequested; + if (requested == Long.MAX_VALUE) { + return; + } + externalRequested = Operators.addCap(n, requested); + } + + requestN(); + } + + private void requestN() { + final long r; + final Subscription s; + + synchronized (this) { + s = internalSubscription; + if (s == null) { + return; + } + + final long er = externalRequested; + final long p = prefetch; + final int pendingFulfil = pendingToFulfil; + + if (er != Long.MAX_VALUE || p != Integer.MAX_VALUE) { + // shortcut + if (pendingFulfil == p) { + return; + } + + r = Math.min(p - pendingFulfil, er); + if (er != Long.MAX_VALUE) { + externalRequested -= r; + } + if (p != Integer.MAX_VALUE) { + pendingToFulfil += r; + } + } else { + r = Long.MAX_VALUE; + } + } + + if (r > 0) { + s.request(r); + } + } + + public void cancel() { + if (!isCanceled() && CANCELED.compareAndSet(this, NOT_CANCELED_STATE, CANCELED_STATE)) { + Subscription s; + + synchronized (this) { + s = internalSubscription; + internalSubscription = null; + subscribed = false; + } + + if (s != null) { + s.cancel(); + } + } + } + + private boolean isCanceled() { + return canceled == CANCELED_STATE; + } + + private class InnerOperator implements CoreSubscriber, Subscription { + final Subscriber destination; + + private InnerOperator(Subscriber destination) { + this.destination = destination; + } + + @Override + public void onSubscribe(Subscription s) { + synchronized (RateLimitableRequestPublisher.this) { + RateLimitableRequestPublisher.this.internalSubscription = s; + + if (isCanceled()) { + s.cancel(); + subscribed = false; + RateLimitableRequestPublisher.this.internalSubscription = null; + } + } + + requestN(); + } + + @Override + public void onNext(T t) { + try { + destination.onNext(t); + + if (prefetch == Integer.MAX_VALUE) { + return; + } + + final long l = limit; + int d = deliveredElements + 1; + + if (d == l) { + d = 0; + final long r; + final Subscription s; + + synchronized (RateLimitableRequestPublisher.this) { + long er = externalRequested; + s = internalSubscription; + + if (s == null) { + return; + } + + if (er >= l) { + er -= l; + // keep pendingToFulfil as is since it is eq to prefetch + r = l; + } else { + pendingToFulfil -= l; + if (er > 0) { + r = er; + er = 0; + pendingToFulfil += r; + } else { + r = 0; + } + } + + externalRequested = er; + } + + if (r > 0) { + s.request(r); + } + } + + deliveredElements = d; + } catch (Throwable e) { + onError(e); + } + } + + @Override + public void onError(Throwable t) { + destination.onError(t); + } + + @Override + public void onComplete() { + destination.onComplete(); + } + + @Override + public void request(long n) {} + + @Override + public void cancel() { + RateLimitableRequestPublisher.this.cancel(); + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/internal/RateLimitableRequestPublisherTest.java b/rsocket-core/src/test/java/io/rsocket/internal/RateLimitableRequestPublisherTest.java new file mode 100644 index 000000000..af4c528e9 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/internal/RateLimitableRequestPublisherTest.java @@ -0,0 +1,140 @@ +package io.rsocket.internal; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.Duration; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.Consumer; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; + +class RateLimitableRequestPublisherTest { + + @Test + public void testThatRequest1WillBePropagatedUpstream() { + Flux source = + Flux.just(1) + .subscribeOn(Schedulers.parallel()) + .doOnRequest(r -> Assertions.assertThat(r).isLessThanOrEqualTo(128)); + + RateLimitableRequestPublisher rateLimitableRequestPublisher = + RateLimitableRequestPublisher.wrap(source, 128); + + StepVerifier.create(rateLimitableRequestPublisher) + .then(() -> rateLimitableRequestPublisher.request(1)) + .expectNext(1) + .expectComplete() + .verify(Duration.ofMillis(1000)); + } + + @Test + public void testThatRequest256WillBePropagatedToUpstreamWithLimitedRate() { + Flux source = + Flux.range(0, 256) + .subscribeOn(Schedulers.parallel()) + .doOnRequest(r -> Assertions.assertThat(r).isLessThanOrEqualTo(128)); + + RateLimitableRequestPublisher rateLimitableRequestPublisher = + RateLimitableRequestPublisher.wrap(source, 128); + + StepVerifier.create(rateLimitableRequestPublisher) + .then(() -> rateLimitableRequestPublisher.request(256)) + .expectNextCount(256) + .expectComplete() + .verify(Duration.ofMillis(1000)); + } + + @Test + public void testThatRequest256WillBePropagatedToUpstreamWithLimitedRateInFewSteps() { + Flux source = + Flux.range(0, 256) + .subscribeOn(Schedulers.parallel()) + .doOnRequest(r -> Assertions.assertThat(r).isLessThanOrEqualTo(128)); + + RateLimitableRequestPublisher rateLimitableRequestPublisher = + RateLimitableRequestPublisher.wrap(source, 128); + + StepVerifier.create(rateLimitableRequestPublisher) + .then(() -> rateLimitableRequestPublisher.request(10)) + .expectNextCount(5) + .then(() -> rateLimitableRequestPublisher.request(128)) + .expectNextCount(133) + .expectNoEvent(Duration.ofMillis(10)) + .then(() -> rateLimitableRequestPublisher.request(Long.MAX_VALUE)) + .expectNextCount(118) + .expectComplete() + .verify(Duration.ofMillis(1000)); + } + + @Test + public void testThatRequestInRandomFashionWillBePropagatedToUpstreamWithLimitedRateInFewSteps() { + Flux source = + Flux.range(0, 10000000) + .subscribeOn(Schedulers.parallel()) + .doOnRequest(r -> Assertions.assertThat(r).isLessThanOrEqualTo(128)); + + RateLimitableRequestPublisher rateLimitableRequestPublisher = + RateLimitableRequestPublisher.wrap(source, 128); + + StepVerifier.create(rateLimitableRequestPublisher) + .then( + () -> + Flux.interval(Duration.ofMillis(1000)) + .onBackpressureDrop() + .subscribe( + new Consumer() { + int count = 10000000; + + @Override + public void accept(Long __) { + int random = ThreadLocalRandom.current().nextInt(1, 512); + + long request = Math.min(random, count); + + count -= request; + + rateLimitableRequestPublisher.request(count); + } + })) + .expectNextCount(10000000) + .expectComplete() + .verify(Duration.ofMillis(30000)); + } + + @Test + public void testThatRequestLongMaxValueWillBeDeliveredInSeparateChunks() { + Flux source = + Flux.range(0, 10000000) + .subscribeOn(Schedulers.parallel()) + .doOnRequest(r -> Assertions.assertThat(r).isLessThanOrEqualTo(128)); + + RateLimitableRequestPublisher rateLimitableRequestPublisher = + RateLimitableRequestPublisher.wrap(source, 128); + + StepVerifier.create(rateLimitableRequestPublisher) + .then(() -> rateLimitableRequestPublisher.request(Long.MAX_VALUE)) + .expectNextCount(10000000) + .expectComplete() + .verify(Duration.ofMillis(30000)); + } + + @Test + public void testThatRequestLongMaxWithIntegerMaxValuePrefetchWillBeDeliveredAsLongMaxValue() { + Flux source = + Flux.range(0, 10000000) + .subscribeOn(Schedulers.parallel()) + .doOnRequest(r -> Assertions.assertThat(r).isEqualTo(Long.MAX_VALUE)); + + RateLimitableRequestPublisher rateLimitableRequestPublisher = + RateLimitableRequestPublisher.wrap(source, Integer.MAX_VALUE); + + StepVerifier.create(rateLimitableRequestPublisher) + .then(() -> rateLimitableRequestPublisher.request(Long.MAX_VALUE)) + .expectNextCount(10000000) + .expectComplete() + .verify(Duration.ofMillis(30000)); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java index fd48cd9d3..6298b0c3a 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java @@ -27,6 +27,7 @@ import org.slf4j.LoggerFactory; import reactor.core.publisher.DirectProcessor; import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; @@ -40,7 +41,9 @@ public class TestDuplexConnection implements DuplexConnection { private final LinkedBlockingQueue sent; private final DirectProcessor sentPublisher; + private final FluxSink sendSink; private final DirectProcessor received; + private final FluxSink receivedSink; private final MonoProcessor onClose; private final ConcurrentLinkedQueue> sendSubscribers; private volatile double availability = 1; @@ -49,7 +52,9 @@ public class TestDuplexConnection implements DuplexConnection { public TestDuplexConnection() { sent = new LinkedBlockingQueue<>(); received = DirectProcessor.create(); + receivedSink = received.sink(); sentPublisher = DirectProcessor.create(); + sendSink = sentPublisher.sink(); sendSubscribers = new ConcurrentLinkedQueue<>(); onClose = MonoProcessor.create(); } @@ -65,7 +70,7 @@ public Mono send(Publisher frames) { .doOnNext( frame -> { sent.offer(frame); - sentPublisher.onNext(frame); + sendSink.next(frame); }) .doOnError(throwable -> logger.error("Error in send stream on test connection.", throwable)) .subscribe(subscriber); @@ -116,7 +121,7 @@ public Publisher getSentAsPublisher() { public void addToReceivedBuffer(ByteBuf... received) { for (ByteBuf frame : received) { - this.received.onNext(frame); + this.receivedSink.next(frame); } } diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java index c7dfe34c6..19c29061b 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java @@ -114,7 +114,7 @@ public void startup() { requestCount = new AtomicInteger(); disconnectionCounter = new CountDownLatch(1); - TcpServerTransport serverTransport = TcpServerTransport.create(0); + TcpServerTransport serverTransport = TcpServerTransport.create("localhost", 0); server = RSocketFactory.receive() diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java index 6c8f0e8fa..7a30a7fd1 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java @@ -21,7 +21,7 @@ public class InteractionsLoadTest { @Test @SlowTest public void channel() { - TcpServerTransport serverTransport = TcpServerTransport.create(0); + TcpServerTransport serverTransport = TcpServerTransport.create("localhost", 0); CloseableChannel server = RSocketFactory.receive() diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java index 41e437fee..9e7f5b0a7 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java @@ -46,7 +46,7 @@ public class TcpIntegrationTest { @Before public void startup() { - TcpServerTransport serverTransport = TcpServerTransport.create(0); + TcpServerTransport serverTransport = TcpServerTransport.create("localhost", 0); server = RSocketFactory.receive() .acceptor((setup, sendingSocket) -> Mono.just(new RSocketProxy(handler))) diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java index 62d7da336..575993c18 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java @@ -57,7 +57,7 @@ public void startup() { this.responseMessage = responseMessage.toString(); this.metaData = metaData.toString(); - TcpServerTransport serverTransport = TcpServerTransport.create(randomPort); + TcpServerTransport serverTransport = TcpServerTransport.create("localhost", randomPort); server = RSocketFactory.receive() .fragment(frameSize) diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java index 53b164247..b40f35e51 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java @@ -38,7 +38,7 @@ public static void main(String... args) { serverRSocketFactory .frameDecoder(PayloadDecoder.ZERO_COPY) .acceptor(new PingHandler()) - .transport(TcpServerTransport.create(port)) + .transport(TcpServerTransport.create("localhost", port)) .start() .block() .onClose() diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/TcpServerTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/TcpServerTransportTest.java index 84c185e26..b6cbfea34 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/TcpServerTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/TcpServerTransportTest.java @@ -70,7 +70,7 @@ void createNullTcpClient() { @DisplayName("creates server with port") @Test void createPort() { - assertThat(TcpServerTransport.create(8000)).isNotNull(); + assertThat(TcpServerTransport.create("localhost", 8000)).isNotNull(); } @DisplayName("creates client with TcpServer") @@ -97,7 +97,7 @@ void start() { @Test void startNullAcceptor() { assertThatNullPointerException() - .isThrownBy(() -> TcpServerTransport.create(8000).start(null, 0)) + .isThrownBy(() -> TcpServerTransport.create("localhost", 8000).start(null, 0)) .withMessage("acceptor must not be null"); } } From ac68ce318ffc9074ae98131bfb1c9870fef4b0d5 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 16 Aug 2019 20:27:47 +0300 Subject: [PATCH 083/181] provides autosnapshoting Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- build.gradle | 8 +++++++- ci/travis.sh | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 3ce473f9c..c24bc5c36 100644 --- a/build.gradle +++ b/build.gradle @@ -46,7 +46,13 @@ subprojects { googleJavaFormat { toolVersion = '1.6' } - + + ext { + if (project.hasProperty('versionSuffix')) { + project.version += project.getProperty('versionSuffix') + } + } + dependencyManagement { imports { mavenBom "io.projectreactor:reactor-bom:${ext['reactor-bom.version']}" diff --git a/ci/travis.sh b/ci/travis.sh index 372c01070..9154da33b 100755 --- a/ci/travis.sh +++ b/ci/travis.sh @@ -11,6 +11,7 @@ elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ] && [ "$bin ./gradlew \ -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" \ -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" \ + -PversionSuffix="-SNAPSHOT" \ build artifactoryPublish --stacktrace elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ] && [ "$bintrayUser" != "" ] ; then From 573c5a84bb04f88bf175ed4a5077576707e4820d Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 20 Aug 2019 21:24:52 +0300 Subject: [PATCH 084/181] moves to OpenJDK 8 builds (#684) Signed-off-by: Oleh Dokuka --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c1e802935..116d2d2ff 100644 --- a/.travis.yml +++ b/.travis.yml @@ -20,7 +20,7 @@ dist: trusty matrix: include: - - jdk: oraclejdk8 + - jdk: openjdk8 - jdk: openjdk11 env: SKIP_RELEASE=true - jdk: openjdk12 From 2795c2f4bc788ec03670b57512a246e860978005 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 20 Aug 2019 21:51:50 +0300 Subject: [PATCH 085/181] BoM project support (#683) * drafts bom project support Signed-off-by: Oleh Dokuka * fixes authors list to uptodate one Signed-off-by: Oleh Dokuka --- AUTHORS | 1 + build.gradle | 10 ++++ rsocket-bom/build.gradle | 121 +++++++++++++++++++++++++++++++++++++++ settings.gradle | 1 + 4 files changed, 133 insertions(+) create mode 100755 rsocket-bom/build.gradle diff --git a/AUTHORS b/AUTHORS index 89f6e3696..ef7dd9dda 100644 --- a/AUTHORS +++ b/AUTHORS @@ -18,3 +18,4 @@ somasun = somasun stevegury = Steve Gury tmontgomery = Todd L. Montgomery yschimke = Yuri Schimke +OlegDokuka = Oleh Dokuka diff --git a/build.gradle b/build.gradle index c24bc5c36..ed66c41cc 100644 --- a/build.gradle +++ b/build.gradle @@ -183,6 +183,16 @@ subprojects { name 'Yuri Schimke' email 'yuri@schimke.ee' } + developer { + id 'OlegDokuka' + name 'Oleh Dokuka' + email 'oleh@netifi.com' + } + developer { + id 'mostroverkhov' + name 'Maksym Ostroverkhov' + email 'm.ostroverkhov@gmail.com' + } } scm { diff --git a/rsocket-bom/build.gradle b/rsocket-bom/build.gradle new file mode 100755 index 000000000..046b8f30d --- /dev/null +++ b/rsocket-bom/build.gradle @@ -0,0 +1,121 @@ +/* + * Copyright 2015-2019 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' + id 'maven-publish' + id 'com.jfrog.artifactory' + id 'com.jfrog.bintray' + id 'io.morethan.jmhreport' + id 'me.champeau.gradle.jmh' +} + +description = 'Bill of materials to make sure a consistent set of versions is used for RSocket-Java.' +configurations.archives.artifacts.clear() + +build.doLast { + pom { + customizePom(it, project) + } +} + +dependencies { + compile project(':rsocket-core') + compile project(':rsocket-test') + compile project(':rsocket-load-balancer') + compile project(':rsocket-micrometer') + compile project(':rsocket-transport-local') + compile project(':rsocket-transport-netty') +} + +def customizePom(generatedPom, gradleProject) { + //make sure that dependencies are under + generatedPom.withXml { + if (generatedPom.generatedDependencies.size > 0) { + asNode().appendNode('dependencyManagement', asNode().dependencies) + asNode().dependencies.replaceNode {} + } + } + + generatedPom.project { + name = 'RSocket-Java Release Train - BOM' + description = gradleProject.description + url = 'http://rsocket.io' + groupId = group + packaging = "pom" + licenses { + license { + name = 'The Apache License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id 'robertroeser' + name 'Robert Roeser' + email 'robert@netifi.com' + } + developer { + id 'rdegnan' + name 'Ryland Degnan' + email 'ryland@netifi.com' + } + developer { + id 'yschimke' + name 'Yuri Schimke' + email 'yuri@schimke.ee' + } + developer { + id 'OlegDokuka' + name 'Oleh Dokuka' + email 'oleh@netifi.com' + } + developer { + id 'mostroverkhov' + name 'Maksym Ostroverkhov' + email 'm.ostroverkhov@gmail.com' + } + } + scm { + connection = 'scm:git:git://github.com/rsocket/rsocket-java.git' + developerConnection = 'scm:git:ssh://github.com/rsocket/rsocket-java.git' + url = 'http://github.com/rsocket/rsocket-java/' + } + issueManagement { + system = "GitHub Issues" + url = "https://github.com/rsocket/rsocket-java/issues" + } + } + + generatedPom.writeTo("$buildDir/poms/rsocket-bom-${version}.xml") +} +plugins.withType(MavenPublishPlugin) { + publishing { + publications { + mavenJava(MavenPublication) { + pom.withXml { + def sb = asString() + sb.setLength 0 + sb.append file("$buildDir/poms/rsocket-bom-${version}.xml").text + println(sb.toString()) + } + } + } + } +} + +publish.dependsOn("build") +publishToMavenLocal.dependsOn("build") \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 16630076a..625633774 100644 --- a/settings.gradle +++ b/settings.gradle @@ -23,3 +23,4 @@ include 'rsocket-micrometer' include 'rsocket-test' include 'rsocket-transport-local' include 'rsocket-transport-netty' +include 'rsocket-bom' From d1a8e6e385f73a4ce8b89d7503d8e2ad743f594a Mon Sep 17 00:00:00 2001 From: Jacky Chan Date: Sat, 31 Aug 2019 01:45:53 +0800 Subject: [PATCH 086/181] Implement Routing Metadata Extension (#687) * Tagging metadata test Signed-off-by: linux_china * Routing Metadata extension Signed-off-by: linux_china * Tagging metadata Signed-off-by: linux_china * format with Google Java Style Signed-off-by: linux_china * optimize read tag name Signed-off-by: linux_china * add ByteBufAllocator Signed-off-by: linux_china * Polishing with ByteBufAllocator introduced Signed-off-by: linux_china * Tagging metadata flyweight Signed-off-by: linux_china * remove allocator Signed-off-by: linux_china * remove construct method with allocator Signed-off-by: linux_china * Add createTaggingMetadata from entry Signed-off-by: linux_china * Api adjusted Signed-off-by: linux_china * performance polishing from robertroeser Signed-off-by: linux_china * Fix tag empty bug Signed-off-by: linux_china --- .../io/rsocket/metadata/RoutingMetadata.java | 18 +++++ .../io/rsocket/metadata/TaggingMetadata.java | 64 ++++++++++++++++ .../metadata/TaggingMetadataFlyweight.java | 76 +++++++++++++++++++ .../rsocket/metadata/TaggingMetadataTest.java | 47 ++++++++++++ 4 files changed, 205 insertions(+) create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/RoutingMetadata.java create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadata.java create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataFlyweight.java create mode 100644 rsocket-core/src/test/java/io/rsocket/metadata/TaggingMetadataTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/RoutingMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/RoutingMetadata.java new file mode 100644 index 000000000..d1f2643dc --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/RoutingMetadata.java @@ -0,0 +1,18 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; + +/** + * Routing Metadata extension from + * https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md + * + * @author linux_china + */ +public class RoutingMetadata extends TaggingMetadata { + private static final WellKnownMimeType ROUTING_MIME_TYPE = + WellKnownMimeType.MESSAGE_RSOCKET_ROUTING; + + public RoutingMetadata(ByteBuf content) { + super(ROUTING_MIME_TYPE.getString(), content); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadata.java new file mode 100644 index 000000000..e22d97106 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadata.java @@ -0,0 +1,64 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import java.nio.charset.StandardCharsets; +import java.util.Iterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Tagging metadata from https://github.com/rsocket/rsocket/blob/master/Extensions/Routing.md + * + * @author linux_china + */ +public class TaggingMetadata implements Iterable, CompositeMetadata.Entry { + /** Tag max length in bytes */ + private static int TAG_LENGTH_MAX = 0xFF; + + private String mimeType; + private ByteBuf content; + + public TaggingMetadata(String mimeType, ByteBuf content) { + this.mimeType = mimeType; + this.content = content; + } + + public Stream stream() { + return StreamSupport.stream( + Spliterators.spliteratorUnknownSize( + iterator(), Spliterator.DISTINCT | Spliterator.NONNULL | Spliterator.ORDERED), + false); + } + + @Override + public Iterator iterator() { + return new Iterator() { + @Override + public boolean hasNext() { + return content.readerIndex() < content.capacity(); + } + + @Override + public String next() { + int tagLength = TAG_LENGTH_MAX & content.readByte(); + if (tagLength > 0) { + return content.readSlice(tagLength).toString(StandardCharsets.UTF_8); + } else { + return ""; + } + } + }; + } + + @Override + public ByteBuf getContent() { + return this.content; + } + + @Override + public String getMimeType() { + return this.mimeType; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataFlyweight.java new file mode 100644 index 000000000..c7870bf0d --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataFlyweight.java @@ -0,0 +1,76 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import java.nio.charset.StandardCharsets; +import java.util.Collection; + +/** + * A flyweight class that can be used to encode/decode tagging metadata information to/from {@link + * ByteBuf}. This is intended for low-level efficient manipulation of such buffers. See {@link + * TaggingMetadata} for an Iterator-like approach to decoding entries. + * + * @author linux_china + */ +public class TaggingMetadataFlyweight { + /** Tag max length in bytes */ + private static int TAG_LENGTH_MAX = 0xFF; + + /** + * create routing metadata + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param tags tag values + * @return routing metadata + */ + public static RoutingMetadata createRoutingMetadata( + ByteBufAllocator allocator, Collection tags) { + return new RoutingMetadata(createTaggingContent(allocator, tags)); + } + + /** + * create tagging metadata from composite metadata entry + * + * @param entry composite metadata entry + * @return tagging metadata + */ + public static TaggingMetadata createTaggingMetadata(CompositeMetadata.Entry entry) { + return new TaggingMetadata(entry.getMimeType(), entry.getContent()); + } + + /** + * create tagging metadata + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param knownMimeType the {@link WellKnownMimeType} to encode. + * @param tags tag values + * @return Tagging Metadata + */ + public static TaggingMetadata createTaggingMetadata( + ByteBufAllocator allocator, String knownMimeType, Collection tags) { + return new TaggingMetadata(knownMimeType, createTaggingContent(allocator, tags)); + } + + /** + * create tagging content + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param tags tag values + * @return tagging content + */ + public static ByteBuf createTaggingContent(ByteBufAllocator allocator, Collection tags) { + CompositeByteBuf taggingContent = allocator.compositeBuffer(); + for (String key : tags) { + int length = ByteBufUtil.utf8Bytes(key); + if (length == 0 || length > TAG_LENGTH_MAX) { + continue; + } + ByteBuf byteBuf = allocator.buffer().writeByte(length); + byteBuf.writeCharSequence(key, StandardCharsets.UTF_8); + taggingContent.addComponent(true, byteBuf); + } + return taggingContent; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/TaggingMetadataTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/TaggingMetadataTest.java new file mode 100644 index 000000000..d1fbb50b0 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/TaggingMetadataTest.java @@ -0,0 +1,47 @@ +package io.rsocket.metadata; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.netty.buffer.ByteBufAllocator; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import org.junit.jupiter.api.Test; + +/** + * Tagging metadata test + * + * @author linux_china + */ +public class TaggingMetadataTest { + private ByteBufAllocator byteBufAllocator = ByteBufAllocator.DEFAULT; + + @Test + public void testParseTags() { + List tags = + Arrays.asList( + "ws://localhost:8080/rsocket", String.join("", Collections.nCopies(129, "x"))); + TaggingMetadata taggingMetadata = + TaggingMetadataFlyweight.createTaggingMetadata( + byteBufAllocator, "message/x.rsocket.routing.v0", tags); + TaggingMetadata taggingMetadataCopy = + new TaggingMetadata("message/x.rsocket.routing.v0", taggingMetadata.getContent()); + assertThat(tags) + .containsExactlyElementsOf(taggingMetadataCopy.stream().collect(Collectors.toList())); + } + + @Test + public void testEmptyTagAndOverLengthTag() { + List tags = + Arrays.asList( + "ws://localhost:8080/rsocket", "", String.join("", Collections.nCopies(256, "x"))); + TaggingMetadata taggingMetadata = + TaggingMetadataFlyweight.createTaggingMetadata( + byteBufAllocator, "message/x.rsocket.routing.v0", tags); + TaggingMetadata taggingMetadataCopy = + new TaggingMetadata("message/x.rsocket.routing.v0", taggingMetadata.getContent()); + assertThat(tags.subList(0, 1)) + .containsExactlyElementsOf(taggingMetadataCopy.stream().collect(Collectors.toList())); + } +} From 6d7b99ae7dc36ac3ddb3a96c463899a39be8d546 Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Fri, 30 Aug 2019 11:24:38 -0700 Subject: [PATCH 087/181] update read me --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6eb8ab527..7b7bc94be 100644 --- a/README.md +++ b/README.md @@ -23,10 +23,10 @@ Example: ```groovy dependencies { - implementation 'io.rsocket:rsocket-core:1.0.0-RC2' - implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC2' -// implementation 'io.rsocket:rsocket-core:1.0.0-RC3-SNAPSHOT' -// implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC3-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:1.0.0-RC3' + implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC3' +// implementation 'io.rsocket:rsocket-core:1.0.0-RC4-SNAPSHOT' +// implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC4-SNAPSHOT' } ``` From c291a198a9ba6d2ecfc2e6bd3f24352e387c21ab Mon Sep 17 00:00:00 2001 From: Robert Roeser Date: Fri, 30 Aug 2019 12:53:33 -0700 Subject: [PATCH 088/181] update version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 433d1c709..a677784c5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-RC3 +version=1.0.0-RC4 From 956acdcd2d4890a4f17fe1d76dff5ceddd12d352 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 1 Sep 2019 23:27:03 +0300 Subject: [PATCH 089/181] fixes bom publication Signed-off-by: Oleh Dokuka --- rsocket-bom/build.gradle | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/rsocket-bom/build.gradle b/rsocket-bom/build.gradle index 046b8f30d..bf08f5a80 100755 --- a/rsocket-bom/build.gradle +++ b/rsocket-bom/build.gradle @@ -32,29 +32,23 @@ build.doLast { } } -dependencies { - compile project(':rsocket-core') - compile project(':rsocket-test') - compile project(':rsocket-load-balancer') - compile project(':rsocket-micrometer') - compile project(':rsocket-transport-local') - compile project(':rsocket-transport-netty') -} - -def customizePom(generatedPom, gradleProject) { - //make sure that dependencies are under - generatedPom.withXml { - if (generatedPom.generatedDependencies.size > 0) { - asNode().appendNode('dependencyManagement', asNode().dependencies) - asNode().dependencies.replaceNode {} +dependencyManagement { + dependencies { + rootProject.subprojects.each { + if (it.name != project.name) { + dependency(group: it.group, name: it.name, version: it.version) + } } } +} +def customizePom(generatedPom, gradleProject) { generatedPom.project { name = 'RSocket-Java Release Train - BOM' description = gradleProject.description url = 'http://rsocket.io' - groupId = group + groupId = gradleProject.group + version = gradleProject.version.trim() packaging = "pom" licenses { license { @@ -105,13 +99,7 @@ def customizePom(generatedPom, gradleProject) { plugins.withType(MavenPublishPlugin) { publishing { publications { - mavenJava(MavenPublication) { - pom.withXml { - def sb = asString() - sb.setLength 0 - sb.append file("$buildDir/poms/rsocket-bom-${version}.xml").text - println(sb.toString()) - } + mavenBom(MavenPublication) { } } } From 68832938d85eb4d6603d14d8f8b0a0d90a818b08 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 2 Sep 2019 00:40:40 +0300 Subject: [PATCH 090/181] fixes bom publication Signed-off-by: Oleh Dokuka --- build.gradle | 215 +++++++++++++++++++++++++-------------- rsocket-bom/build.gradle | 84 +-------------- 2 files changed, 145 insertions(+), 154 deletions(-) diff --git a/build.gradle b/build.gradle index ed66c41cc..d3ea08183 100644 --- a/build.gradle +++ b/build.gradle @@ -102,103 +102,168 @@ subprojects { } } - plugins.withType(JavaPlugin) { - compileJava { - sourceCompatibility = 1.8 + if (project.name != 'rsocket-bom') { - // TODO: Cleanup warnings so no need to exclude - options.compilerArgs << '-Xlint:all,-overloads,-rawtypes,-unchecked' - } + plugins.withType(JavaPlugin) { + compileJava { + sourceCompatibility = 1.8 - javadoc { - options.with { - links 'https://docs.oracle.com/javase/8/docs/api/' - links 'https://projectreactor.io/docs/core/release/api/' - links 'https://netty.io/4.1/api/' + // TODO: Cleanup warnings so no need to exclude + options.compilerArgs << '-Xlint:all,-overloads,-rawtypes,-unchecked' } - } - test { - useJUnitPlatform() + javadoc { + options.with { + links 'https://docs.oracle.com/javase/8/docs/api/' + links 'https://projectreactor.io/docs/core/release/api/' + links 'https://netty.io/4.1/api/' + } + } - systemProperty "io.netty.leakDetection.level", "ADVANCED" - } - - tasks.named("javadoc").configure { - onlyIf { System.getenv('SKIP_RELEASE') != "true" } - } - } + test { + useJUnitPlatform() - plugins.withType(JavaLibraryPlugin) { - task sourcesJar(type: Jar) { - classifier 'sources' - from sourceSets.main.allJava + systemProperty "io.netty.leakDetection.level", "ADVANCED" + } + + tasks.named("javadoc").configure { + onlyIf { System.getenv('SKIP_RELEASE') != "true" } + } } - task javadocJar(type: Jar, dependsOn: javadoc) { - classifier 'javadoc' - from javadoc.destinationDir + plugins.withType(JavaLibraryPlugin) { + task sourcesJar(type: Jar) { + classifier 'sources' + from sourceSets.main.allJava + } + + task javadocJar(type: Jar, dependsOn: javadoc) { + classifier 'javadoc' + from javadoc.destinationDir + } } - } - plugins.withType(MavenPublishPlugin) { - publishing { - publications { - maven(MavenPublication) { - groupId 'io.rsocket' + plugins.withType(MavenPublishPlugin) { + publishing { + publications { + maven(MavenPublication) { + groupId 'io.rsocket' - from components.java + from components.java - artifact sourcesJar - artifact javadocJar + artifact sourcesJar + artifact javadocJar - pom.withXml { - asNode().':version' + { - resolveStrategy = DELEGATE_FIRST + pom.withXml { + asNode().':version' + { + resolveStrategy = DELEGATE_FIRST - name project.name - description project.description - url 'http://rsocket.io' + name project.name + description project.description + url 'http://rsocket.io' - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/license/LICENSE-2.0.txt' + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/license/LICENSE-2.0.txt' + } } - } - developers { - developer { - id 'robertroeser' - name 'Robert Roeser' - email 'robert@netifi.com' - } - developer { - id 'rdegnan' - name 'Ryland Degnan' - email 'ryland@netifi.com' + developers { + developer { + id 'robertroeser' + name 'Robert Roeser' + email 'robert@netifi.com' + } + developer { + id 'rdegnan' + name 'Ryland Degnan' + email 'ryland@netifi.com' + } + developer { + id 'yschimke' + name 'Yuri Schimke' + email 'yuri@schimke.ee' + } + developer { + id 'OlegDokuka' + name 'Oleh Dokuka' + email 'oleh@netifi.com' + } + developer { + id 'mostroverkhov' + name 'Maksym Ostroverkhov' + email 'm.ostroverkhov@gmail.com' + } } - developer { - id 'yschimke' - name 'Yuri Schimke' - email 'yuri@schimke.ee' + + scm { + connection 'scm:git:https://github.com/rsocket/rsocket-java.git' + developerConnection 'scm:git:https://github.com/rsocket/rsocket-java.git' + url 'https://github.com/rsocket/rsocket-java' } - developer { - id 'OlegDokuka' - name 'Oleh Dokuka' - email 'oleh@netifi.com' + } + } + } + } + } + } + + } else { + plugins.withType(MavenPublishPlugin) { + publishing { + publications { + maven(MavenPublication) { + groupId 'io.rsocket' + + pom.withXml { + asNode().':version' + { + resolveStrategy = DELEGATE_FIRST + + name project.name + description project.description + url 'http://rsocket.io' + + licenses { + license { + name 'The Apache Software License, Version 2.0' + url 'http://www.apache.org/license/LICENSE-2.0.txt' + } } - developer { - id 'mostroverkhov' - name 'Maksym Ostroverkhov' - email 'm.ostroverkhov@gmail.com' + + developers { + developer { + id 'robertroeser' + name 'Robert Roeser' + email 'robert@netifi.com' + } + developer { + id 'rdegnan' + name 'Ryland Degnan' + email 'ryland@netifi.com' + } + developer { + id 'yschimke' + name 'Yuri Schimke' + email 'yuri@schimke.ee' + } + developer { + id 'OlegDokuka' + name 'Oleh Dokuka' + email 'oleh@netifi.com' + } + developer { + id 'mostroverkhov' + name 'Maksym Ostroverkhov' + email 'm.ostroverkhov@gmail.com' + } } - } - scm { - connection 'scm:git:https://github.com/rsocket/rsocket-java.git' - developerConnection 'scm:git:https://github.com/rsocket/rsocket-java.git' - url 'https://github.com/rsocket/rsocket-java' + scm { + connection 'scm:git:https://github.com/rsocket/rsocket-java.git' + developerConnection 'scm:git:https://github.com/rsocket/rsocket-java.git' + url 'https://github.com/rsocket/rsocket-java' + } } } } diff --git a/rsocket-bom/build.gradle b/rsocket-bom/build.gradle index bf08f5a80..73e4047fc 100755 --- a/rsocket-bom/build.gradle +++ b/rsocket-bom/build.gradle @@ -14,96 +14,22 @@ * limitations under the License. */ plugins { - id 'java-library' - id 'maven' + id 'java' id 'maven-publish' id 'com.jfrog.artifactory' id 'com.jfrog.bintray' - id 'io.morethan.jmhreport' - id 'me.champeau.gradle.jmh' + id 'io.spring.dependency-management' } description = 'Bill of materials to make sure a consistent set of versions is used for RSocket-Java.' -configurations.archives.artifacts.clear() - -build.doLast { - pom { - customizePom(it, project) - } -} +//configurations.archives.artifacts.clear() dependencyManagement { dependencies { rootProject.subprojects.each { if (it.name != project.name) { - dependency(group: it.group, name: it.name, version: it.version) + dependency(group: it.group, name: it.name, version: it.version) } } } -} - -def customizePom(generatedPom, gradleProject) { - generatedPom.project { - name = 'RSocket-Java Release Train - BOM' - description = gradleProject.description - url = 'http://rsocket.io' - groupId = gradleProject.group - version = gradleProject.version.trim() - packaging = "pom" - licenses { - license { - name = 'The Apache License, Version 2.0' - url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' - } - } - developers { - developer { - id 'robertroeser' - name 'Robert Roeser' - email 'robert@netifi.com' - } - developer { - id 'rdegnan' - name 'Ryland Degnan' - email 'ryland@netifi.com' - } - developer { - id 'yschimke' - name 'Yuri Schimke' - email 'yuri@schimke.ee' - } - developer { - id 'OlegDokuka' - name 'Oleh Dokuka' - email 'oleh@netifi.com' - } - developer { - id 'mostroverkhov' - name 'Maksym Ostroverkhov' - email 'm.ostroverkhov@gmail.com' - } - } - scm { - connection = 'scm:git:git://github.com/rsocket/rsocket-java.git' - developerConnection = 'scm:git:ssh://github.com/rsocket/rsocket-java.git' - url = 'http://github.com/rsocket/rsocket-java/' - } - issueManagement { - system = "GitHub Issues" - url = "https://github.com/rsocket/rsocket-java/issues" - } - } - - generatedPom.writeTo("$buildDir/poms/rsocket-bom-${version}.xml") -} -plugins.withType(MavenPublishPlugin) { - publishing { - publications { - mavenBom(MavenPublication) { - } - } - } -} - -publish.dependsOn("build") -publishToMavenLocal.dependsOn("build") \ No newline at end of file +} \ No newline at end of file From 55b847bd900e538a93fc1c7b17798811c34aa2a6 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 2 Sep 2019 00:54:57 +0300 Subject: [PATCH 091/181] fixes bom publication Signed-off-by: Oleh Dokuka --- rsocket-bom/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-bom/build.gradle b/rsocket-bom/build.gradle index 73e4047fc..8230ef2f3 100755 --- a/rsocket-bom/build.gradle +++ b/rsocket-bom/build.gradle @@ -28,7 +28,7 @@ dependencyManagement { dependencies { rootProject.subprojects.each { if (it.name != project.name) { - dependency(group: it.group, name: it.name, version: it.version) + dependency(group: 'io.rsocket', name: it.name, version: it.version) } } } From 1472c16bdbc223e52a4a99be643b69609725c6e8 Mon Sep 17 00:00:00 2001 From: Igor Grishin Date: Mon, 2 Sep 2019 20:16:15 +0300 Subject: [PATCH 092/181] Fix readme example code syntax (#690) Signed-off-by: bigspawn --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7b7bc94be..173c3e1ad 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ Frames can be printed out to help debugging. Set the logger `io.rsocket.FrameLog ## Trivial Client -``` +```java package io.rsocket.transport.netty; import io.rsocket.Payload; From c6b3d64f9b5e8db7e6203d9518739671739db657 Mon Sep 17 00:00:00 2001 From: liyixin <601947961@qq.com> Date: Tue, 3 Sep 2019 01:16:45 +0800 Subject: [PATCH 093/181] add @Override on override method (#689) Signed-off-by: Administrator <601947961@qq.com> --- .../src/main/java/io/rsocket/ConnectionSetupPayload.java | 2 ++ rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java | 2 ++ rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java | 2 ++ 3 files changed, 6 insertions(+) diff --git a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java index 44bcaf6a1..8762e0489 100644 --- a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java @@ -61,8 +61,10 @@ public ConnectionSetupPayload retain(int increment) { return this; } + @Override public abstract ConnectionSetupPayload touch(); + @Override public abstract ConnectionSetupPayload touch(Object hint); private static final class DefaultConnectionSetupPayload extends ConnectionSetupPayload { diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java index 2e94e9881..66c68009a 100644 --- a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java +++ b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java @@ -44,6 +44,7 @@ class Tuple2ByteBuf extends AbstractTupleByteBuf { this.freed = false; } + @Override long calculateRelativeIndex(int index) { checkIndex(index, 0); @@ -60,6 +61,7 @@ long calculateRelativeIndex(int index) { return relativeIndex | mask; } + @Override ByteBuf getPart(int index) { long ri = calculateRelativeIndex(index); switch ((int) ((ri & MASK) >>> 32L)) { diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java index 151675746..d02b22586 100644 --- a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java +++ b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java @@ -57,6 +57,7 @@ public boolean isDirect() { return one.isDirect() && two.isDirect() && three.isDirect(); } + @Override public long calculateRelativeIndex(int index) { checkIndex(index, 0); long relativeIndex; @@ -75,6 +76,7 @@ public long calculateRelativeIndex(int index) { return relativeIndex | mask; } + @Override public ByteBuf getPart(int index) { long ri = calculateRelativeIndex(index); switch ((int) ((ri & MASK) >>> 32L)) { From 3fe5750a3b3bdf4733b606bbaa82d582ae55dd97 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 2 Sep 2019 20:17:20 +0300 Subject: [PATCH 094/181] Performance Optimization Session (#682) * imporves UnicastMonoProcessor performance by reducing inner MonoProcessor Signed-off-by: Oleh Dokuka * provides tests and some minor fixes to UnicastMonoProcessor Signed-off-by: Oleh Dokuka * fixes format Signed-off-by: Oleh Dokuka --- .../internal/UnicastMonoProcessor.java | 309 +++++--- .../internal/UnicastMonoProcessorTest.java | 661 ++++++++++++++++++ 2 files changed, 878 insertions(+), 92 deletions(-) create mode 100644 rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java index 35d4906ec..d5958028a 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java @@ -1,175 +1,300 @@ package io.rsocket.internal; import java.util.Objects; +import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.stream.Stream; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.reactivestreams.Processor; +import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; +import reactor.core.Exceptions; import reactor.core.Scannable; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Operators; import reactor.util.annotation.Nullable; import reactor.util.context.Context; -import reactor.util.function.Tuple2; public class UnicastMonoProcessor extends Mono implements Processor, CoreSubscriber, Disposable, Subscription, Scannable { + /** + * Create a {@link UnicastMonoProcessor} that will eagerly request 1 on {@link + * #onSubscribe(Subscription)}, cache and emit the eventual result for 1 or N subscribers. + * + * @param type of the expected value + * @return A {@link UnicastMonoProcessor}. + */ + public static UnicastMonoProcessor create() { + return new UnicastMonoProcessor<>(); + } + + volatile CoreSubscriber actual; + + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater ACTUAL = + AtomicReferenceFieldUpdater.newUpdater( + UnicastMonoProcessor.class, CoreSubscriber.class, "actual"); + + volatile int once; + @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater ONCE = AtomicIntegerFieldUpdater.newUpdater(UnicastMonoProcessor.class, "once"); - private final MonoProcessor processor; + Throwable error; + volatile boolean terminated; + O value; - @SuppressWarnings("unused") - private volatile int once; + volatile Subscription subscription; + static final AtomicReferenceFieldUpdater UPSTREAM = + AtomicReferenceFieldUpdater.newUpdater( + UnicastMonoProcessor.class, Subscription.class, "subscription"); - private UnicastMonoProcessor() { - this.processor = MonoProcessor.create(); - } + @Override + public final void cancel() { + if (isTerminated()) { + return; + } - public static UnicastMonoProcessor create() { - return new UnicastMonoProcessor<>(); - } + final Subscription s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription()); + if (s == Operators.cancelledSubscription()) { + return; + } - @Override - public Stream actuals() { - return processor.actuals(); + if (s != null) { + s.cancel(); + } } @Override - public boolean isScanAvailable() { - return processor.isScanAvailable(); - } + @SuppressWarnings("unchecked") + public void dispose() { + final Subscription s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription()); + if (s == Operators.cancelledSubscription()) { + return; + } - @Override - public String name() { - return processor.name(); - } + final CancellationException e = new CancellationException("Disposed"); + error = e; + value = null; + terminated = true; + if (s != null) { + s.cancel(); + } - @Override - public String stepName() { - return processor.stepName(); + final CoreSubscriber a = this.actual; + ACTUAL.lazySet(this, null); + if (a != null) { + a.onError(e); + } } - @Override - public Stream steps() { - return processor.steps(); + /** + * Return the produced {@link Throwable} error if any or null + * + * @return the produced {@link Throwable} error if any or null + */ + @Nullable + public final Throwable getError() { + return isTerminated() ? error : null; } - @Override - public Stream parents() { - return processor.parents(); + /** + * Indicates whether this {@code UnicastMonoProcessor} has been interrupted via cancellation. + * + * @return {@code true} if this {@code UnicastMonoProcessor} is cancelled, {@code false} + * otherwise. + */ + public boolean isCancelled() { + return isDisposed() && !isTerminated(); } - @Override - @Nullable - public T scan(Attr key) { - return processor.scan(key); + /** + * Indicates whether this {@code UnicastMonoProcessor} has been completed with an error. + * + * @return {@code true} if this {@code UnicastMonoProcessor} was completed with an error, {@code + * false} otherwise. + */ + public final boolean isError() { + return getError() != null; } - @Override - public T scanOrDefault(Attr key, T defaultValue) { - return processor.scanOrDefault(key, defaultValue); + /** + * Indicates whether this {@code UnicastMonoProcessor} has been terminated by the source producer + * with a success or an error. + * + * @return {@code true} if this {@code UnicastMonoProcessor} is successful, {@code false} + * otherwise. + */ + public final boolean isTerminated() { + return terminated; } @Override - public Stream> tags() { - return processor.tags(); + public boolean isDisposed() { + return subscription == Operators.cancelledSubscription(); } @Override - public void onSubscribe(Subscription s) { - processor.onSubscribe(s); + public final void onComplete() { + onNext(null); } @Override - public void onNext(O o) { - processor.onNext(o); - } + @SuppressWarnings("unchecked") + public final void onError(Throwable cause) { + Objects.requireNonNull(cause, "onError cannot be null"); + + if (UPSTREAM.getAndSet(this, Operators.cancelledSubscription()) + == Operators.cancelledSubscription()) { + Operators.onErrorDropped(cause, currentContext()); + return; + } - @Override - public void onError(Throwable t) { - processor.onError(t); - } + error = cause; + value = null; + terminated = true; - @Nullable - public Throwable getError() { - return processor.getError(); + final CoreSubscriber a = actual; + ACTUAL.lazySet(this, null); + if (a != null) { + a.onError(cause); + } } - public boolean isCancelled() { - return processor.isCancelled(); - } + @Override + @SuppressWarnings("unchecked") + public final void onNext(@Nullable O value) { + final Subscription s; + if ((s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription())) + == Operators.cancelledSubscription()) { + if (value != null) { + Operators.onNextDropped(value, currentContext()); + } + return; + } - public boolean isError() { - return processor.isError(); - } + this.value = value; + terminated = true; - public boolean isSuccess() { - return processor.isSuccess(); + final CoreSubscriber a = actual; + ACTUAL.lazySet(this, null); + if (value == null) { + if (a != null) { + a.onComplete(); + } + } else { + if (s != null) { + s.cancel(); + } + + if (a != null) { + a.onNext(value); + a.onComplete(); + } + } } - public boolean isTerminated() { - return processor.isTerminated(); + @Override + public final void onSubscribe(Subscription subscription) { + if (Operators.setOnce(UPSTREAM, this, subscription)) { + subscription.request(Long.MAX_VALUE); + } } + /** + * Returns the value that completed this {@link UnicastMonoProcessor}. Returns {@code null} if the + * {@link UnicastMonoProcessor} has not been completed. If the {@link UnicastMonoProcessor} is + * completed with an error a RuntimeException that wraps the error is thrown. + * + * @return the value that completed the {@link UnicastMonoProcessor}, or {@code null} if it has + * not been completed + * @throws RuntimeException if the {@link UnicastMonoProcessor} was completed with an error + */ @Nullable public O peek() { - return processor.peek(); - } - - public long downstreamCount() { - return processor.downstreamCount(); - } - - public boolean hasDownstreams() { - return processor.hasDownstreams(); - } + if (!isTerminated()) { + return null; + } - @Override - public void onComplete() { - processor.onComplete(); - } + if (value != null) { + return value; + } - @Override - public void request(long n) { - processor.request(n); - } + if (error != null) { + RuntimeException re = Exceptions.propagate(error); + re = Exceptions.addSuppressed(re, new Exception("Mono#peek terminated with an error")); + throw re; + } - @Override - public void cancel() { - processor.cancel(); + return null; } @Override - public void dispose() { - processor.dispose(); + public final void request(long n) { + Operators.validate(n); } @Override public Context currentContext() { - return processor.currentContext(); + final CoreSubscriber a = this.actual; + return a != null ? a.currentContext() : Context.empty(); } @Override - public boolean isDisposed() { - return processor.isDisposed(); + @Nullable + public Object scanUnsafe(Attr key) { + // touch guard + boolean c = isCancelled(); + + if (key == Attr.TERMINATED) { + return isTerminated(); + } + if (key == Attr.PARENT) { + return subscription; + } + if (key == Attr.ERROR) { + return error; + } + if (key == Attr.PREFETCH) { + return Integer.MAX_VALUE; + } + if (key == Attr.CANCELLED) { + return c; + } + return null; } - @Override - public Object scanUnsafe(Attr key) { - return processor.scanUnsafe(key); + /** + * Return true if any {@link Subscriber} is actively subscribed + * + * @return true if any {@link Subscriber} is actively subscribed + */ + public final boolean hasDownstream() { + return actual != null; } @Override public void subscribe(CoreSubscriber actual) { Objects.requireNonNull(actual, "subscribe"); if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { - processor.subscribe(actual); + actual.onSubscribe(this); + ACTUAL.lazySet(this, actual); + if (isTerminated()) { + Throwable ex = error; + if (ex != null) { + actual.onError(ex); + } else { + O v = value; + if (v != null) { + actual.onNext(v); + } + actual.onComplete(); + } + ACTUAL.lazySet(this, null); + } } else { Operators.error( actual, diff --git a/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java b/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java new file mode 100644 index 000000000..20ed4b469 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java @@ -0,0 +1,661 @@ +package io.rsocket.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assumptions.assumeThat; + +import java.lang.ref.WeakReference; +import java.time.Duration; +import java.util.Date; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.Scannable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Operators; +import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; +import reactor.util.function.Tuple2; + +public class UnicastMonoProcessorTest { + + @Test + public void noRetentionOnTermination() throws InterruptedException { + Date date = new Date(); + CompletableFuture future = new CompletableFuture<>(); + + WeakReference refDate = new WeakReference<>(date); + WeakReference> refFuture = new WeakReference<>(future); + + Mono source = Mono.fromFuture(future); + Mono data = + source.map(Date::toString).log().subscribeWith(UnicastMonoProcessor.create()).log(); + + future.complete(date); + assertThat(data.block()).isEqualTo(date.toString()); + + date = null; + future = null; + source = null; + System.gc(); + + int cycles; + for (cycles = 10; cycles > 0; cycles--) { + if (refDate.get() == null && refFuture.get() == null) break; + Thread.sleep(100); + } + + assumeThat(refFuture.get()).isNull(); + assertThat(refDate.get()).isNull(); + assertThat(cycles).isNotZero().isPositive(); + } + + @Test + public void noRetentionOnTerminationError() throws InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + + WeakReference> refFuture = new WeakReference<>(future); + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + + Mono source = Mono.fromFuture(future); + Mono data = source.map(Date::toString).subscribeWith(processor); + + future.completeExceptionally(new IllegalStateException()); + + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(data::block); + + future = null; + source = null; + System.gc(); + + int cycles; + for (cycles = 10; cycles > 0; cycles--) { + if (refFuture.get() == null) break; + Thread.sleep(100); + } + + assumeThat(refFuture.get()).isNull(); + assertThat(cycles).isNotZero().isPositive(); + } + + @Test + public void noRetentionOnTerminationCancel() throws InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + + WeakReference> refFuture = new WeakReference<>(future); + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + + Mono source = Mono.fromFuture(future); + Mono data = + source.map(Date::toString).transformDeferred((s) -> s.subscribeWith(processor)); + + future = null; + source = null; + + data.subscribe().dispose(); + processor.dispose(); + + data = null; + + System.gc(); + + int cycles; + for (cycles = 10; cycles > 0; cycles--) { + if (refFuture.get() == null) break; + Thread.sleep(100); + } + + assumeThat(refFuture.get()).isNull(); + assertThat(cycles).isNotZero().isPositive(); + } + + @Test(expected = IllegalStateException.class) + public void MonoProcessorResultNotAvailable() { + MonoProcessor mp = MonoProcessor.create(); + mp.block(Duration.ofMillis(1)); + } + + @Test + public void MonoProcessorRejectedDoOnSuccessOrError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + mp.doOnSuccessOrError((s, f) -> ref.set(f)).subscribe(); + mp.onError(new Exception("test")); + + assertThat(ref.get()).hasMessage("test"); + assertThat(mp.isError()).isTrue(); + } + + @Test + public void MonoProcessorRejectedDoOnTerminate() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicInteger invoked = new AtomicInteger(); + + mp.doOnTerminate(invoked::incrementAndGet).subscribe(); + mp.onError(new Exception("test")); + + assertThat(invoked.get()).isEqualTo(1); + assertThat(mp.isError()).isTrue(); + } + + @Test + public void MonoProcessorRejectedSubscribeCallback() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + mp.subscribe(v -> {}, ref::set); + mp.onError(new Exception("test")); + + assertThat(ref.get()).hasMessage("test"); + assertThat(mp.isError()).isTrue(); + } + + @Test + public void MonoProcessorSuccessDoOnSuccessOrError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + mp.doOnSuccessOrError((s, f) -> ref.set(s)).subscribe(); + mp.onNext("test"); + + assertThat(ref.get()).isEqualToIgnoringCase("test"); + assertThat(mp.isTerminated()).isTrue(); + assertThat(mp.isError()).isFalse(); + } + + @Test + public void MonoProcessorSuccessDoOnTerminate() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicInteger invoked = new AtomicInteger(); + + mp.doOnTerminate(invoked::incrementAndGet).subscribe(); + mp.onNext("test"); + + assertThat(invoked.get()).isEqualTo(1); + assertThat(mp.isTerminated()).isTrue(); + assertThat(mp.isError()).isFalse(); + } + + @Test + public void MonoProcessorSuccessSubscribeCallback() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + mp.subscribe(ref::set); + mp.onNext("test"); + + assertThat(ref.get()).isEqualToIgnoringCase("test"); + assertThat(mp.isTerminated()).isTrue(); + assertThat(mp.isError()).isFalse(); + } + + @Test + public void MonoProcessorRejectedDoOnError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + mp.doOnError(ref::set).subscribe(); + mp.onError(new Exception("test")); + + assertThat(ref.get()).hasMessage("test"); + assertThat(mp.isError()).isTrue(); + } + + @Test(expected = NullPointerException.class) + public void MonoProcessorRejectedSubscribeCallbackNull() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.subscribe((Subscriber) null); + } + + @Test + public void MonoProcessorSuccessDoOnSuccess() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + mp.doOnSuccess(ref::set).subscribe(); + mp.onNext("test"); + + assertThat(ref.get()).isEqualToIgnoringCase("test"); + assertThat(mp.isTerminated()).isTrue(); + assertThat(mp.isError()).isFalse(); + } + + @Test + public void MonoProcessorSuccessChainTogether() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + mp.subscribe(mp2); + + mp.onNext("test"); + + assertThat(mp2.peek()).isEqualToIgnoringCase("test"); + assertThat(mp.isTerminated()).isTrue(); + assertThat(mp.isError()).isFalse(); + } + + @Test + public void MonoProcessorRejectedChainTogether() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + mp.subscribe(mp2); + + mp.onError(new Exception("test")); + + assertThat(mp2.getError()).hasMessage("test"); + assertThat(mp.isError()).isTrue(); + } + + @Test + public void MonoProcessorDoubleFulfill() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + StepVerifier.create(mp) + .then( + () -> { + mp.onNext("test1"); + mp.onNext("test2"); + }) + .expectNext("test1") + .expectComplete() + .verifyThenAssertThat() + .hasDroppedExactly("test2"); + } + + @Test + public void MonoProcessorNullFulfill() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.onNext(null); + + assertThat(mp.isTerminated()).isTrue(); + assertThat(mp.peek()).isNull(); + } + + @Test + public void MonoProcessorMapFulfill() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.onNext(1); + + UnicastMonoProcessor mp2 = + mp.map(s -> s * 2).subscribeWith(UnicastMonoProcessor.create()); + mp2.subscribe(); + + assertThat(mp2.isTerminated()).isTrue(); + assertThat(mp2.peek()).isEqualTo(2); + } + + @Test + public void MonoProcessorThenFulfill() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.onNext(1); + + UnicastMonoProcessor mp2 = + mp.flatMap(s -> Mono.just(s * 2)).subscribeWith(UnicastMonoProcessor.create()); + mp2.subscribe(); + + assertThat(mp2.isTerminated()).isTrue(); + assertThat(mp2.peek()).isEqualTo(2); + } + + @Test + public void MonoProcessorMapError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.onNext(1); + + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + + StepVerifier.create( + mp.map( + s -> { + throw new RuntimeException("test"); + }) + .subscribeWith(mp2), + 0) + .thenRequest(1) + .then( + () -> { + assertThat(mp2.isTerminated()).isTrue(); + assertThat(mp2.getError()).hasMessage("test"); + }) + .verifyErrorMessage("test"); + } + + @Test(expected = Exception.class) + public void MonoProcessorDoubleError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.onError(new Exception("test")); + mp.onError(new Exception("test")); + } + + @Test(expected = Exception.class) + public void MonoProcessorDoubleSignal() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.onNext("test"); + mp.onError(new Exception("test")); + } + + @Test + public void zipMonoProcessor() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + UnicastMonoProcessor> mp3 = UnicastMonoProcessor.create(); + + StepVerifier.create(Mono.zip(mp, mp2).subscribeWith(mp3)) + .then(() -> assertThat(mp3.isTerminated()).isFalse()) + .then(() -> mp.onNext(1)) + .then(() -> assertThat(mp3.isTerminated()).isFalse()) + .then(() -> mp2.onNext(2)) + .then( + () -> { + assertThat(mp3.isTerminated()).isTrue(); + assertThat(mp3.peek().getT1()).isEqualTo(1); + assertThat(mp3.peek().getT2()).isEqualTo(2); + }) + .expectNextMatches(t -> t.getT1() == 1 && t.getT2() == 2) + .verifyComplete(); + } + + @Test + public void zipMonoProcessor2() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp3 = UnicastMonoProcessor.create(); + + StepVerifier.create(Mono.zip(d -> (Integer) d[0], mp).subscribeWith(mp3)) + .then(() -> assertThat(mp3.isTerminated()).isFalse()) + .then(() -> mp.onNext(1)) + .then( + () -> { + assertThat(mp3.isTerminated()).isTrue(); + assertThat(mp3.peek()).isEqualTo(1); + }) + .expectNext(1) + .verifyComplete(); + } + + @Test + public void zipMonoProcessorRejected() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + UnicastMonoProcessor> mp3 = UnicastMonoProcessor.create(); + + StepVerifier.create(Mono.zip(mp, mp2).subscribeWith(mp3)) + .then(() -> assertThat(mp3.isTerminated()).isFalse()) + .then(() -> mp.onError(new Exception("test"))) + .then( + () -> { + assertThat(mp3.isTerminated()).isTrue(); + assertThat(mp3.getError()).hasMessage("test"); + }) + .verifyErrorMessage("test"); + } + + @Test + public void filterMonoProcessor() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + StepVerifier.create(mp.filter(s -> s % 2 == 0).subscribeWith(mp2)) + .then(() -> mp.onNext(2)) + .then(() -> assertThat(mp2.isError()).isFalse()) + .then(() -> assertThat(mp2.isTerminated()).isTrue()) + .then(() -> assertThat(mp2.peek()).isEqualTo(2)) + .then(() -> assertThat(mp2.isTerminated()).isTrue()) + .expectNext(2) + .verifyComplete(); + } + + @Test + public void filterMonoProcessorNot() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + StepVerifier.create(mp.filter(s -> s % 2 == 0).subscribeWith(mp2)) + .then(() -> mp.onNext(1)) + .then(() -> assertThat(mp2.isError()).isFalse()) + .then(() -> assertThat(mp2.isTerminated()).isTrue()) + .then(() -> assertThat(mp2.peek()).isNull()) + .then(() -> assertThat(mp2.isTerminated()).isTrue()) + .verifyComplete(); + } + + @Test + public void filterMonoProcessorError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + StepVerifier.create( + mp.filter( + s -> { + throw new RuntimeException("test"); + }) + .subscribeWith(mp2)) + .then(() -> mp.onNext(2)) + .then(() -> assertThat(mp2.isError()).isTrue()) + .then(() -> assertThat(mp2.isTerminated()).isTrue()) + .then(() -> assertThat(mp2.getError()).hasMessage("test")) + .then(() -> assertThat(mp2.isTerminated()).isTrue()) + .verifyErrorMessage("test"); + } + + @Test + public void doOnSuccessMonoProcessorError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + StepVerifier.create( + mp.doOnSuccess( + s -> { + throw new RuntimeException("test"); + }) + .doOnError(ref::set) + .subscribeWith(mp2)) + .then(() -> mp.onNext(2)) + .then(() -> assertThat(mp2.isError()).isTrue()) + .then(() -> assertThat(ref.get()).hasMessage("test")) + .then(() -> assertThat(mp2.isTerminated()).isTrue()) + .then(() -> assertThat(mp2.getError()).hasMessage("test")) + .then(() -> assertThat(mp2.isTerminated()).isTrue()) + .verifyErrorMessage("test"); + } + + @Test + public void fluxCancelledByMonoProcessor() { + AtomicLong cancelCounter = new AtomicLong(); + Flux.range(1, 10) + .doOnCancel(cancelCounter::incrementAndGet) + .transformDeferred((s) -> s.subscribeWith(UnicastMonoProcessor.create())) + .subscribe(); + + assertThat(cancelCounter.get()).isEqualTo(1); + } + + @Test + public void cancelledByMonoProcessor() { + AtomicLong cancelCounter = new AtomicLong(); + UnicastMonoProcessor monoProcessor = + Mono.just("foo") + .doOnCancel(cancelCounter::incrementAndGet) + .subscribeWith(UnicastMonoProcessor.create()); + monoProcessor.subscribe(); + + assertThat(cancelCounter.get()).isEqualTo(1); + } + + @Test + public void scanProcessor() { + UnicastMonoProcessor test = UnicastMonoProcessor.create(); + Subscription subscription = Operators.emptySubscription(); + test.onSubscribe(subscription); + + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); + assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); + + test.onComplete(); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); + assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); + } + + @Test + public void scanProcessorCancelled() { + UnicastMonoProcessor test = UnicastMonoProcessor.create(); + Subscription subscription = Operators.emptySubscription(); + test.onSubscribe(subscription); + + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); + assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); + + test.cancel(); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); + assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); + } + + @Test + public void scanProcessorSubscription() { + UnicastMonoProcessor test = UnicastMonoProcessor.create(); + Subscription subscription = Operators.emptySubscription(); + test.onSubscribe(subscription); + + assertThat(test.scan(Scannable.Attr.ACTUAL)).isNull(); + assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(subscription); + } + + @Test + public void scanProcessorError() { + UnicastMonoProcessor test = UnicastMonoProcessor.create(); + Subscription subscription = Operators.emptySubscription(); + test.onSubscribe(subscription); + + test.onError(new IllegalStateException("boom")); + + assertThat(test.scan(Scannable.Attr.ERROR)).hasMessage("boom"); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); + } + + @Test + public void monoToProcessorConnects() { + TestPublisher tp = TestPublisher.create(); + UnicastMonoProcessor connectedProcessor = + tp.mono().subscribeWith(UnicastMonoProcessor.create()); + + assertThat(connectedProcessor.subscription).isNotNull(); + } + + @Test + public void monoToProcessorChain() { + StepVerifier.withVirtualTime( + () -> + Mono.just("foo") + .subscribeWith(UnicastMonoProcessor.create()) + .delayElement(Duration.ofMillis(500))) + .expectSubscription() + .expectNoEvent(Duration.ofMillis(500)) + .expectNext("foo") + .verifyComplete(); + } + + @Test + public void monoToProcessorChainColdToHot() { + AtomicInteger subscriptionCount = new AtomicInteger(); + Mono coldToHot = + Mono.just("foo") + .doOnSubscribe(sub -> subscriptionCount.incrementAndGet()) + .transformDeferred(s -> s.subscribeWith(UnicastMonoProcessor.create())) + .subscribeWith(UnicastMonoProcessor.create()) // this actually subscribes + .filter(s -> s.length() < 4); + + assertThat(subscriptionCount.get()).isEqualTo(1); + + coldToHot.block(); + assertThatThrownBy(coldToHot::block) + .hasMessage("UnicastMonoProcessor allows only a single Subscriber"); + assertThatThrownBy(coldToHot::block) + .hasMessage("UnicastMonoProcessor allows only a single Subscriber"); + + assertThat(subscriptionCount.get()).isEqualTo(1); + } + + @Test + public void monoProcessorBlockIsUnbounded() { + long start = System.nanoTime(); + + String result = + Mono.just("foo") + .delayElement(Duration.ofMillis(500)) + .subscribeWith(UnicastMonoProcessor.create()) + .block(); + + assertThat(result).isEqualTo("foo"); + assertThat(Duration.ofNanos(System.nanoTime() - start)) + .isGreaterThanOrEqualTo(Duration.ofMillis(500)); + } + + @Test + public void monoProcessorBlockNegativeIsImmediateTimeout() { + long start = System.nanoTime(); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy( + () -> + Mono.just("foo") + .delayElement(Duration.ofMillis(500)) + .subscribeWith(UnicastMonoProcessor.create()) + .block(Duration.ofSeconds(-1))) + .withMessage("Timeout on blocking read for -1000 MILLISECONDS"); + + assertThat(Duration.ofNanos(System.nanoTime() - start)).isLessThan(Duration.ofMillis(500)); + } + + @Test + public void monoProcessorBlockZeroIsImmediateTimeout() { + long start = System.nanoTime(); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy( + () -> + Mono.just("foo") + .delayElement(Duration.ofMillis(500)) + .subscribeWith(UnicastMonoProcessor.create()) + .block(Duration.ZERO)) + .withMessage("Timeout on blocking read for 0 MILLISECONDS"); + + assertThat(Duration.ofNanos(System.nanoTime() - start)).isLessThan(Duration.ofMillis(500)); + } + + @Test + public void disposeBeforeValueSendsCancellationException() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AtomicReference e1 = new AtomicReference<>(); + AtomicReference e2 = new AtomicReference<>(); + AtomicReference e3 = new AtomicReference<>(); + AtomicReference late = new AtomicReference<>(); + + processor.subscribe(v -> Assertions.fail("expected first subscriber to error"), e1::set); + processor.subscribe(v -> Assertions.fail("expected second subscriber to error"), e2::set); + processor.subscribe(v -> Assertions.fail("expected third subscriber to error"), e3::set); + + processor.dispose(); + + assertThat(e1.get()).isInstanceOf(CancellationException.class); + assertThat(e2.get()).isInstanceOf(IllegalStateException.class); + assertThat(e3.get()).isInstanceOf(IllegalStateException.class); + + processor.subscribe(v -> Assertions.fail("expected late subscriber to error"), late::set); + assertThat(late.get()).isInstanceOf(IllegalStateException.class); + } +} From d9e494458be66fefbe7ef8477057c03309938a44 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Tue, 17 Sep 2019 15:27:06 +0300 Subject: [PATCH 095/181] update resumable file transfer example Signed-off-by: Maksym Ostroverkhov --- .../examples/transport/tcp/resume/ResumeFileTransfer.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 df8e801a6..ca115d281 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 @@ -17,6 +17,8 @@ import reactor.core.publisher.Mono; public class ResumeFileTransfer { + /*amount of file chunks requested by subscriber: n, refilled on n/2 of received items*/ + private static final int PREFETCH_WINDOW_SIZE = 4; public static void main(String[] args) { RequestCodec requestCodec = new RequestCodec(); @@ -43,7 +45,7 @@ public static void main(String[] args) { client .requestStream(requestCodec.encode(new Request(16, "lorem.txt"))) .doFinally(s -> server.dispose()) - .subscribe(Files.fileSink("rsocket-examples/out/lorem_output.txt", 256)); + .subscribe(Files.fileSink("rsocket-examples/out/lorem_output.txt", PREFETCH_WINDOW_SIZE)); server.onClose().block(); } From 47ad4d66e55626ab64a9b7d216c662fd96955ab7 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 25 Sep 2019 16:27:48 +0200 Subject: [PATCH 096/181] Upgrade to Reactor Dysprosium (#697) Signed-off-by: Brian Clozel --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index d3ea08183..ccb256434 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,7 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = 'Dysprosium-M3' + ext['reactor-bom.version'] = 'Dysprosium-RELEASE' ext['logback.version'] = '1.2.3' ext['findbugs.version'] = '3.0.2' ext['netty.version'] = '4.1.37.Final' @@ -95,7 +95,6 @@ subprojects { repositories { mavenCentral() - maven { url 'http://repo.spring.io/milestone' } // temporary for Reactor Dysprosium if (version.endsWith('BUILD-SNAPSHOT') || project.hasProperty('platformVersion')) { maven { url 'http://repo.spring.io/libs-snapshot' } From c4c8d22555e2e5c7214350d73a299b75f59c28c4 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Wed, 25 Sep 2019 16:45:41 +0200 Subject: [PATCH 097/181] Build refactoring and fixing published POMs (#696) * Upgrade to Gradle 5.6.2 Signed-off-by: Brian Clozel * Do not publish dependency management with POMs Prior to this commit, the POMs and BOM produced by this build would also publish dependency management information for artifacts that are not provided by RSocket. This is problematic since this would provide/enforce opinions about unrelated dependencies such as test and logging dependencies. This commit configures the dependency-management Gradle plugin to not publish dependency management information with the generated POMs. It also uses the java-platform Gradle plugin to produce a proper BOM that does not contain dependencies, publishes dependency management information for RSocket artifacts only, and does not provide any scope opinion about those. This commit also polishes the main build file and: * uses the Netty BOM * upgrades JUnit to its latest version and uses its BOM * refactors the Gradle build files to avoid duplication Signed-off-by: Brian Clozel --- build.gradle | 224 ++++-------------- .../artifactory.gradle | 0 bintray.gradle => gradle/bintray.gradle | 0 gradle/publications.gradle | 65 +++++ gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 56177 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- rsocket-bom/build.gradle | 28 ++- 7 files changed, 132 insertions(+), 187 deletions(-) rename artifactory.gradle => gradle/artifactory.gradle (100%) rename bintray.gradle => gradle/bintray.gradle (100%) create mode 100644 gradle/publications.gradle diff --git a/build.gradle b/build.gradle index ccb256434..60aef840a 100644 --- a/build.gradle +++ b/build.gradle @@ -15,9 +15,9 @@ */ plugins { - id 'com.gradle.build-scan' version '1.16' + id 'com.gradle.build-scan' version '2.4.2' - id 'com.github.sherter.google-java-format' version '0.7.1' apply false + id 'com.github.sherter.google-java-format' version '0.8' apply false id 'com.jfrog.artifactory' version '4.7.3' apply false id 'com.jfrog.bintray' version '1.8.4' apply false id 'me.champeau.gradle.jmh' version '0.4.8' apply false @@ -28,17 +28,18 @@ plugins { subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - + apply from: "${rootDir}/gradle/publications.gradle" + ext['reactor-bom.version'] = 'Dysprosium-RELEASE' ext['logback.version'] = '1.2.3' ext['findbugs.version'] = '3.0.2' - ext['netty.version'] = '4.1.37.Final' + ext['netty-bom.version'] = '4.1.37.Final' ext['netty-boringssl.version'] = '2.0.25.Final' ext['hdrhistogram.version'] = '2.1.10' ext['mockito.version'] = '2.25.1' ext['slf4j.version'] = '1.7.25' ext['jmh.version'] = '1.21' - ext['junit.version'] = '5.1.0' + ext['junit.version'] = '5.5.2' ext['hamcrest.version'] = '1.3' ext['micrometer.version'] = '1.0.6' ext['assertj.version'] = '3.11.1' @@ -48,49 +49,41 @@ subprojects { } ext { - if (project.hasProperty('versionSuffix')) { - project.version += project.getProperty('versionSuffix') - } + if (project.hasProperty('versionSuffix')) { + project.version += project.getProperty('versionSuffix') + } } dependencyManagement { imports { mavenBom "io.projectreactor:reactor-bom:${ext['reactor-bom.version']}" + mavenBom "io.netty:netty-bom:${ext['netty-bom.version']}" + mavenBom "org.junit:junit-bom:${ext['junit.version']}" } dependencies { dependency "ch.qos.logback:logback-classic:${ext['logback.version']}" dependency "com.google.code.findbugs:jsr305:${ext['findbugs.version']}" - dependency "io.netty:netty-buffer:${ext['netty.version']}" dependency "io.netty:netty-tcnative-boringssl-static:${ext['netty-boringssl.version']}" dependency "io.micrometer:micrometer-core:${ext['micrometer.version']}" dependency "org.assertj:assertj-core:${ext['assertj.version']}" dependency "org.hdrhistogram:HdrHistogram:${ext['hdrhistogram.version']}" dependency "org.slf4j:slf4j-api:${ext['slf4j.version']}" - dependencySet(group: 'org.mockito', version: ext['mockito.version']) { entry 'mockito-junit-jupiter' entry 'mockito-core' } - - dependencySet(group: 'org.junit.jupiter', version: ext['junit.version']) { - entry 'junit-jupiter-api' - entry 'junit-jupiter-engine' - entry 'junit-jupiter-params' - } - // TODO: Remove after JUnit5 migration dependency 'junit:junit:4.12' dependency "org.hamcrest:hamcrest-library:${ext['hamcrest.version']}" - dependencySet(group: 'org.junit.vintage', version: ext['junit.version']) { - entry 'junit-vintage-engine' - } - dependencySet(group: 'org.openjdk.jmh', version: ext['jmh.version']) { entry 'jmh-core' entry 'jmh-generator-annprocess' } } + generatedPomCustomization { + enabled = false + } } repositories { @@ -101,179 +94,56 @@ subprojects { } } - if (project.name != 'rsocket-bom') { - - plugins.withType(JavaPlugin) { - compileJava { - sourceCompatibility = 1.8 + plugins.withType(JavaPlugin) { + compileJava { + sourceCompatibility = 1.8 - // TODO: Cleanup warnings so no need to exclude - options.compilerArgs << '-Xlint:all,-overloads,-rawtypes,-unchecked' - } - - javadoc { - options.with { - links 'https://docs.oracle.com/javase/8/docs/api/' - links 'https://projectreactor.io/docs/core/release/api/' - links 'https://netty.io/4.1/api/' - } - } - - test { - useJUnitPlatform() - - systemProperty "io.netty.leakDetection.level", "ADVANCED" - } - - tasks.named("javadoc").configure { - onlyIf { System.getenv('SKIP_RELEASE') != "true" } - } + // TODO: Cleanup warnings so no need to exclude + options.compilerArgs << '-Xlint:all,-overloads,-rawtypes,-unchecked' } - plugins.withType(JavaLibraryPlugin) { - task sourcesJar(type: Jar) { - classifier 'sources' - from sourceSets.main.allJava - } - - task javadocJar(type: Jar, dependsOn: javadoc) { - classifier 'javadoc' - from javadoc.destinationDir + javadoc { + options.with { + links 'https://docs.oracle.com/javase/8/docs/api/' + links 'https://projectreactor.io/docs/core/release/api/' + links 'https://netty.io/4.1/api/' } } - plugins.withType(MavenPublishPlugin) { - publishing { - publications { - maven(MavenPublication) { - groupId 'io.rsocket' - - from components.java - - artifact sourcesJar - artifact javadocJar - - pom.withXml { - asNode().':version' + { - resolveStrategy = DELEGATE_FIRST + test { + useJUnitPlatform() - name project.name - description project.description - url 'http://rsocket.io' - - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/license/LICENSE-2.0.txt' - } - } - - developers { - developer { - id 'robertroeser' - name 'Robert Roeser' - email 'robert@netifi.com' - } - developer { - id 'rdegnan' - name 'Ryland Degnan' - email 'ryland@netifi.com' - } - developer { - id 'yschimke' - name 'Yuri Schimke' - email 'yuri@schimke.ee' - } - developer { - id 'OlegDokuka' - name 'Oleh Dokuka' - email 'oleh@netifi.com' - } - developer { - id 'mostroverkhov' - name 'Maksym Ostroverkhov' - email 'm.ostroverkhov@gmail.com' - } - } - - scm { - connection 'scm:git:https://github.com/rsocket/rsocket-java.git' - developerConnection 'scm:git:https://github.com/rsocket/rsocket-java.git' - url 'https://github.com/rsocket/rsocket-java' - } - } - } - } - } - } + systemProperty "io.netty.leakDetection.level", "ADVANCED" } - } else { - plugins.withType(MavenPublishPlugin) { - publishing { - publications { - maven(MavenPublication) { - groupId 'io.rsocket' - - pom.withXml { - asNode().':version' + { - resolveStrategy = DELEGATE_FIRST - - name project.name - description project.description - url 'http://rsocket.io' + tasks.named("javadoc").configure { + onlyIf { System.getenv('SKIP_RELEASE') != "true" } + } + } - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'http://www.apache.org/license/LICENSE-2.0.txt' - } - } + plugins.withType(JavaLibraryPlugin) { + task sourcesJar(type: Jar) { + classifier 'sources' + from sourceSets.main.allJava + } - developers { - developer { - id 'robertroeser' - name 'Robert Roeser' - email 'robert@netifi.com' - } - developer { - id 'rdegnan' - name 'Ryland Degnan' - email 'ryland@netifi.com' - } - developer { - id 'yschimke' - name 'Yuri Schimke' - email 'yuri@schimke.ee' - } - developer { - id 'OlegDokuka' - name 'Oleh Dokuka' - email 'oleh@netifi.com' - } - developer { - id 'mostroverkhov' - name 'Maksym Ostroverkhov' - email 'm.ostroverkhov@gmail.com' - } - } + task javadocJar(type: Jar, dependsOn: javadoc) { + classifier 'javadoc' + from javadoc.destinationDir + } - scm { - connection 'scm:git:https://github.com/rsocket/rsocket-java.git' - developerConnection 'scm:git:https://github.com/rsocket/rsocket-java.git' - url 'https://github.com/rsocket/rsocket-java' - } - } - } - } + publishing { + publications { + maven(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar } } } } -} -apply from: 'artifactory.gradle' -apply from: 'bintray.gradle' +} buildScan { termsOfServiceUrl = 'https://gradle.com/terms-of-service' diff --git a/artifactory.gradle b/gradle/artifactory.gradle similarity index 100% rename from artifactory.gradle rename to gradle/artifactory.gradle diff --git a/bintray.gradle b/gradle/bintray.gradle similarity index 100% rename from bintray.gradle rename to gradle/bintray.gradle diff --git a/gradle/publications.gradle b/gradle/publications.gradle new file mode 100644 index 000000000..c6b118615 --- /dev/null +++ b/gradle/publications.gradle @@ -0,0 +1,65 @@ +apply plugin: "maven-publish" +apply from: "${rootDir}/gradle/artifactory.gradle" +apply from: "${rootDir}/gradle/bintray.gradle" + +plugins.withType(MavenPublishPlugin) { + publishing { + publications { + maven(MavenPublication) { + pom { + name = project.name + groupId = 'io.rsocket' + description = project.description + url = 'http://rsocket.io' + licenses { + license { + name = "The Apache Software License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "repo" + } + } + developers { + developer { + id = 'robertroeser' + name = 'Robert Roeser' + email = 'robert@netifi.com' + } + developer { + id = 'rdegnan' + name = 'Ryland Degnan' + email = 'ryland@netifi.com' + } + developer { + id = 'yschimke' + name = 'Yuri Schimke' + email = 'yuri@schimke.ee' + } + developer { + id = 'OlegDokuka' + name = 'Oleh Dokuka' + email = 'oleh@netifi.com' + } + developer { + id = 'mostroverkhov' + name = 'Maksym Ostroverkhov' + email = 'm.ostroverkhov@gmail.com' + } + } + scm { + connection = 'scm:git:https://github.com/rsocket/rsocket-java.git' + developerConnection = 'scm:git:https://github.com/rsocket/rsocket-java.git' + url = 'https://github.com/rsocket/rsocket-java' + } + versionMapping { + usage('java-api') { + fromResolutionResult() + } + usage('java-runtime') { + fromResolutionResult() + } + } + } + } + } + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f6b961fd5a86aa5fbfe90f707c3138408be7c718..29953ea141f55e3b8fc691d31b5ca8816d89fa87 100644 GIT binary patch delta 49492 zcmY(JQ*__&)3wvEv28nPY}>Z&#>S_yZQFLz*tTsaO}??(JiqlGz5la0-1l0uX6`-L z?yP_SwLv6s!S-QKNQr`hfoUZQtHI&oo#TRBKrY|F67UjRJmAQ<73evkz`&T{k_(7& zlNG)Z0D7KFk|_VMrZi2(zmLLl3!6%lcrbC6rlf_G{UW86`!P@jq;3_IEHM&{e#+u=}8YXMTswO|99-&pAD=(PV38d+=xDyEj4xe8Kj?0=|=l zu!&qt>W=2bypi}gb4Iv&@kBtG54#9{CuV|d1R|nMy-OinA(V zhV44&#C9Tnq*)yECSd>R(1tUm^)ROVj<{y7fBTE6t(3%LAi;zcGR`{8QiO7vmfK2>YBFZ)B_sT6T4zNVNn2c7#JamZOCXzrS-%P?-)Lf!*g{Bu(C=#XGc_T zKk>M)uoUB|DC9B~7S*fnC%3!!t*puyGaR{Bsf)1P+q9YDO6b*xY#k}|BoY12Hs8Zqb2WTp2Jw9W8PFOO^V zUN$^X!PZ-Tz!mVfC#}NGW?ZbsxG`K#CW|yw0=ydq6F^)YR zhXV>vMWsTh;q;<*x5e&w2;gLh$=02C^~AHc!%)mdxl#r%Lgc#%@{0V0B_AD=^{b7v z7IFS*+##2FG2EBSe_$jZS$>*dH~gGYMoCiXYaDN#xn<*5PYX1`fyZ^RhBM-i7#x90 zJr+?-w~QN7zD)Db#6}MLj6P%|&38i8RW8HOz9`{SqGLMV)p;J$9YE{{nMNZES!fC! zr@|}wKV@tk$JzI`B0jZ|v`Ar!erdGHcgYL?tC5ID>Os$uPNRh(lBVRG>*vDcRl%s< z;9u9nG=Dj{BLZ>;!T#Pw9Kr>jl%Q_Rb>OZxVRxy-@I=7jNChQ8I;C1mqp$=^n+W9< z(!gAB&3)sSs|cM7_5$K+kXAaJ+~<=2KqWl}3lteQ#SF3jwvgxkN@k54_ey+JQ(Q%0 zR-t>(vvOmXxIK%Y{wUFBVCAxavLa%b)RP zcsqvPj|U5376A*|*wv?c2~7(Dt#V50B{Iu7V~0%4ns2@jRutFNb6Vk`TCUR8<&3u; z$D(Uc5`O(<)PzJy3T9b!Xt^`=5#3zuz>)+lm8R$@Wc&*(dH8j{&$mNC5`tBT%_=JN z`oo^!XNAMXKE(eDU2`$-O_qmI2Mns3mg}v5P(hlRipnS3qA!)x${Wvkgd+r}7?~d? z_pVE#1HXhOy#hFh5|4HAYOH!=V2rbc5Z}ymwN*xoC@Uo8Q-^gFe&1fYpHaZl{k>tCL%~*Nfs3J9xpobql#S z8V|sb-PIQ|C>U&5NAt;E{7~hfZV;A4urqA@mj~MGS7afkuGm4Jm72{oJ=n%_VzWQGpkAXsH3_u?IBk~EhnU3&`inh z6LQ57at+R@3{91L%6R|NzE`lP_N_=@VDW@tV5G^;&_v0?BhWyu4y-Z8n)kl|n^rd6 z*br@JMjUN~bxT*Wbs#E3%G7y_dafK|0?;NbTM~B z8Yk-`7=N#ao{t5XwVGb8s@|e(1H?G`Dm8(rVY_dioB?C~x6mm4w@`N5}ab-+YEy}^LMKJ}?RA^u+9q{es;GKme8G z^0eo&jAKP67eaSs$XlIl*~WKu@+w+2g-%;P3~4%f5R8*Jf||S<9U?4b1B4N0X_QeT zhD___%DiL~$u9_1yUD>{kfpOJ5@8IQYI5>Awv((7w_Fq$2`5d1DSFG5^oTkZ3vnt<9W^gQ#GypH3f!{zo?D#cTo{g2Xt6iKx#c^l5skrYk$e!?(&cJpLXPdJhW z-XrI+n~05xl4J}>-<{#Ni5*(UA2Dy7Yiv74d1U`f8P(H2)Dd-J)~vVNq|@`CWf2eLPBeh(1y_YrZ8iP_h9qr zC~j-B6uTW3Ad0w_Pb1F?*YM#1NS1yO-yEOip(T#E9vMeU^#1xnpu9hAhQ)31x9y+Lc zRFd1c>P4!*?!_n3878V-xldxt`SErd$2#<}PM$~JR%TzPizd~&ZN@Fd{;--y1EhM0 zoqASZ0vP-@$b5f)syEoCEpSb9GBBcZl*Ee8zutJT@^WuA;>*4})NxJ*2M&B@^lGi% z#2`~gkeA<-G+R9Ez7{BkZe7ZSXjcx`5juBIQ?+k_B+=O~@4?~s+(~2Wq zTO=*dv4%zumaIBtI*n#_+}C&PfUd@hB3*sq)c6X5gH>?RJ`WZ?Id~82jO#^sH{`Ec zEC%x+BDj*WgaZ5y9!$(p!r8!I75fD z?wxw_AP@ZQuvyD~#aV274E?@x5`NQ85}}voJS(z#ZMN@DLi{!Rv7wyD#81gS%9}-# z38D?V1Az|f19Y#=B>$#-uT;5gkKCnMWfK<`dU(f-FVLl#y;h^8;MJ#ldK= z$S~zYF1snV8-|OdpK?COKm%ouONfrOOuje*kF87}LGy73+O@Mcz5t4b@Il$AfYo@y zH#x;|84&})cH}b%MU+V$5`GivAt-#k6#Z=~#;|Rx3EIxR1Q-TyW&6Qjl;sxlNu)eB zk;O4TF~c$`hrA||P*-9mQRRxNJS91-Htx1FLeD@!1)^_sHWL}jK#JU$=gX7Z^EL7d zHLm6XgjD=<7>9gFv@=-__gRMtpQ26jED3Yu9Rfy3k2rVugvn31+qHj4$RIeE;&Qd9 zGRkdy*Y9YydaRQeaOL+1F4KH2Lv2`(uosM(KMh$=`B{BV*)s zG(#KA#!D!Xr^wk|zZU#w5E|O2_@d5T^sZ!$6G>_;=h}0Kqg!+0M#FWV*Yj=^;`ks< zd4-K6;K#gwrK8s7dY19YEa&jl#B*u7P1ABes55P+49LAw0l0Y)KE)5Of>{V-=!V>x zc*IJ|VTC`=s9#bqJc9 ziWvkM#}URXEn~!+C?T@HJx7%}#G2EuQtPcz4?SHaZ$0)QLK(<0jdw}2B2%Gi$<}zx zGSufF&vTw80j$w{khiK@LWpYbNsN#&Uf;Y;$~-YcXBSksSQ0BoG%*H6%vt|zoWTbf zJ5NN{aI;eDuqNqYCWp++&veW9=gw3&F(Ft!WjQ1RxB}=96^bDze8Xc8jtt-^tP);VrfK{FW!2!sICcdOIvlGDjs=`Mo+rw}M2Z z&Z;Hfp%VYidcG|;*S0KYPJ0%46ir4$WPv(yKt-xBVc=h&_~O)~4{ZEKUah`GlPARq zO^BvU>s+hUsG9=Uzcf>eKk{eD?FtwB1DTv_IyOGobTq1~&O))VbR@M_5^aU>&)tJ@}u`2D1^$ zC$uzQ;DbFi&!rTU-=`)gyR6y+#xalo^iWnqi~|nUhR~p)SQv`q7~f<4104tXg1|_X z{0m02%go^dL$s%>#9weHjUkLlJiPkCZlLfD&kvDwk3*fE-5@w0eiUI(<4(ekiGnwrALYCv~LDeijoov*5C^1s< z*b`DCJo-&Ktkt6j%j`sM{g6%u-q|?`){1JOY z$tnb6qao7M)P>@qg|fsf-wr3)@$xeR@^{5PIKt_E4kBojEG+t=BhJ{g2m-;a4)C(| z__hKq`zL2^^F3i}Hce|F#=kQikL9?U{X6=O&8FxR!wfQKac?6*;jr$U&KpwjO`uVZuHASVfLNMCwK5F$(Y<>1jP z%A`c7o267}#_JD3(W#35e`Iv!-cGH7^w0sT{ZfdZIf2v&{5Jb$&_EvRJQOH#j@{K& zkZwVJPvnE0@@bBef?q)`y9l$wo8=BfklsvmI+*%@4 z*Y4+jTwoYGCtS9|XJ@id_M0m<&?awkwcTfeP~jWg?R+^Xb!V;^Nq!-MFA$${LwF`aUHHq>G!&0a`ERSmZZuBAR+Jt&nDi zUQ1FDPdot!Oj4tYpz0lnlh}bFo5!SrGfaZc$y(Ibr}LUx()<)M$A=x^FX6k4p!y?b zHR)v7wXr@_(|4!tKtL zS9ZZ6blu8-0CV%%(KM_6i0k74t}YLoXJWt?1N!z@a@&#XHSKV3PW!WkgANwKu`i;{ zWQF~9M3W6=UYsJ;Pzv1T)sl4}v(N~vz!c-$QRp?MdnozIZ0tZgDC!akso}=vcjU@& zC+0>3DcGMX2e~70S1Qvw)?P)!nXzd!f2Du~Ccm5}0EmcuGzr>H4btAoNDnBEv&3RZ zc@68GV-%e@duUXJ@~EXF_EYw! z3u=3R4KBA+xG}c;#mu^YR8Gd)&mD>IDt3)a3YhoX#~>`fXRqVTBwpdkDJ>JbNm;n_HhPu7*aA#BDx~Konc?W8Q8(@Z;P6%B1xv8R~`x1B*mR2I24l zTdw$8xL<@ii<|en9^1pUSzEj^dJ~bgEl{fF=!YH731)mY#~Ht2W!4rWdqB2YHJZ-T zwaU`4?ck-MoGESdi-JFLZLBJi_5<%A+4zUg8+~xX26T0e#eMc61>>e3* zShZ8%N0o&47>jmY(3Z!gTQ@6hH-yJQ&SH)+hO zAy0=%j@k9mWuQ*8M$Tou>BJZ)B%x1bSz@s&k&-&H6h^_34|Uf5DG7QCFRyak$PBu| zsLG=z6k;>qY-?K2H$;7kyohevr?bgp~ z>+Wn>EM1Y|z#$*s=o%!2x)d|`-u`*&9~S>2O%3UG#5x_Fb|D#s{=WarIH)y*i8yET z+9n+2o?@{_)(}zN&2f|wuu`1Z7#Ml|ZcFg4{e&PO<66qawwO<%KgO`7Szs$$sxv!7 z>1v{jwDD6yP^GoRk*ypsXm^0bCT0D;nZ+XmaVW)-9D93-|xr?g+}3d z1B%kHLEs|JdSjJU#hV^Hx8N{g+f@ea1Z!MydF=i+DGI4l#p?iAmh-?90+i4Z_+M7H zZVbHl$~d)g_-#+>aai$gZ$hD=shZ9>EhmLBNJYEX|0)oC;A*5(2+kiUv5|Yct(^k& zP#qoiQ-vw3?!c#$IM%^kPCqUuDmxT4Uc)i`kd!#1!>TZ&3omqUM8}I)j=Co8_F^Mc zkK3^2S?iVK&+W2VY?D2=gr>Iu%T?z?q3odttN6o2fwV~)pXx;`xg_~i zhtAp!0!aYA=ns<)rgXzR@;LUpR*jz^_DqO|tE%Yy=KHT;P5 zYA2&~Ru#ix%IWJ2o=#+z@-f$cCz>s%lbpFAb>T1USO6(Dz3nJe?q`IZSH)ds8mpxf zpTl%*DHfbMw&FT_d6PSgd@4u^z7C7&MpRkU4mO&{bYgFzQOt7kHDb&7 zARl0M{8og9vAxlgq)8D2iSo##3)B?Q{_w5*IAELX5;X51+P8_~SGa^IIO7$cYePo4 z*!cG?YZpKcAx0W5((mg0BMJX(jQ)Ht2mM2og+EFu#0Vu27xg-&@cT>taQV0dTjCyh z7^g6QTg3TtFP>?1gpxt{sDmpaV=gQcM~h*&;O^9xJv(k~;SC;<=A1%(frh|VeW5QZ zp_==~>$N4!3NmbSH_Kbq)A5HY4v2pHf)T{HnKtQKugRl{ijamm z^9L?vkTmMgwCPdcx?mxoLWXLlcr=I27ov}8ntd==uXQxfyjRaC36AIgvRj#BITjQ5 z4S?CE=nj)G5{-m&hXG{afC=Q+s566!y10|5HMb0NTqM)+FlS6eE;rNj9n(w~1UIhn zF$1dFsDiE+0$%N^HMJa`^L#&|=ie5ZAYjMJDWYRTAvd407M6BFLa@P#>5>LB91u55hKYF^X+ROQBZE^ z6|GBhD*+k7jez>n{{aj_Efxg%^%htCGEY80IPV9squ0>&_{Zg87~=hc1lMO=%@CQt zaM#QI&DBpbi0Bi2)90sYeLr7BT(6Q#N{j>h7DmODD1-F|&1^zI9GBHbyi&dLUYv|y zo>D3-Vj>Ho5~~n~3X#AQ>i_Pz+~)~MA8;_Ru;hGVPGC!MOBh7}p420zbD3Qj4ntN? zdJzrVT8n{ks9{~&F@h~!`9FFP-EomJ|NJvbHwor5d%p9dWZNztFS#yI?_{4I`Y3U3L z9DPnrEWmS#FLY)4&}Eb6+A>z&#KCUJczzM)w6SulOl`IKzs+;oXY8bWFQ9hd@oV;I8S5c* zojV;2*JHoQxzI7)u@3w6ubxbh?lRP2_O+Zi1>ULWYScT*>!GK1=w4R0$@4sRhIB(~ zJkzt^5qd!4{^C{SOV6M-?jm6^F-B zl%6&u^rXl=S?2^^UJJ=ia&PEoj$gwxKv=LP1~N^RJ6PBcd7dCtetW-99ukPM);8Cc zvo%1ZN0>%QozSS|j;G`_jiy%w%VLOSTn1*ughqTTTqVlP?PRPzgp#6?`V9@^y~r%5R#Sq7OcE7Q8M_U7-<>@%Gpk*nfSA^Z$W8 zs)I4CH#jh`ZX7T$y8p-sRQ-eBcV>|kVrb9_079yZN%E2rD_${E*Pnd~V-C1as&P#Y8*w9Zp zNU+8fRjD85RYwsSi^97En4)DXqXUv#LFlk6Bjabo83)`6XWcbH7q z{hG23=nfpw2?ozqd4cDv0U&Qbkr8YVC6tlo9VTY>od%}S&|1%HSjk%%_MYmU!`nNd z_iG9NXPxRy8 zAG2@dR{fI*P`;DGgm_IBzIpDs$t|oC`*`nqCfZzBTXL^(d=d5pjOu?eyQvEm!W8@1 zQ>;6VkT)iB22LWer8PWea8yVohY1%%bT``-^zbdOWS*$yN^UgQciEA^8|1(+lF@CU zMM~7vf>S;r27~7zxn?X{hLf=rbMbhrMRi&^?n zaHFk-7farm>k|_LT!#ZU&mMivR}17Ucqh0=YQ_P72TYS4E`0n#KC2AkpqAOsa((m* zCI=!i1{@MA29~No9TQ^orW6$7!jai!E4ZXTH#&%vaQt)I2z8G{Jq*!j(hJLSQiJjsr$yGtzC}a;5KMFPU8`(J zWGUArCSrOF>~DJiv_8HyH`b7fDF!WqKZXrz&nnm@)0-*!qh@hUrldq!ERkAj5BU3b zc~j9?$q)ezUvATjN*1w+l&g*x2WA5A9l0|e(XVVerJ=}pppcm^D)P<(VI}VGimA#2 z9f?1ZK@AIeByolDu+vkv+l2T^x=0x%n+N6;yxnWx@e`ftqb-~BSCs~g*1gsPF+Wok zB#j;Zj<{BiOKHtUoQp;lKW}OBNO1I3ROyunBd!7ax`^Z*IztK_DnlyU<7w`Vwo7+h zpSgMTk4!^jdj#PvJTlX#Dwc6!zkiy+4*Cw zF!-wz^Nmjxw04jLSSGjUhZ$6gVB=|468dow%R%~xSbx;TGpR5RFtyJQdw*_p7prKN zPMHAZ-mF+0PbQpjXU8RB*hic`Dk*1OjAD&??UGyQux2rlcM{?TR zml@sO2 zP9xuv>e2)(sIUXcu&f7}vFJ<_sj#9cqqKplNDH!ph?6XBQl`QsCgqOD7q{MPh0&Vv zj6{JH+ZuCRJJZzVHP;YJThrDG^!3fcG1D?k9M1DN7AjE^f)(^gXCF?d;TV|3OK7hi z$hhH|M&S&u0}}xg$V*MiOWpg$mfJfOa(kO1Lw4!N*evU$!%vZ}hU_Cni?cWDSZl!2 z+IQwxEShvNB&$L&9fjFYit5qo3&#RkP*#V!K2<{^|6(mQO0%yL3CqtAaw)P=ClWM1 z+Vt7^^u6CoHpx3v^dU zmb?euGvcc47A`*sw_|wU1Xz_O0vCV|@5M~6Djn6+3I9ej7ENWNQ&nA^y&uWZJ=5~p zKAl&u&fl>7Gx3kW32T{Wx=q!UTE?W3%p|9OXV~iWx}0*O;mVBK990#?8Q8~ktIFL} zT9&PL#1ZnCd-NTba++#)w*Lv5-qGTZpt<0Y*{{OgX5rCaSKq>edv=kQ)4c(jOx`cy zoo%k8qT}h*(=59CoLrpncHWB;%_N(ZdM%)|%o^8r!Z$lZn_#>C1jX$9NuJI<@X7KN zSO>=#Y6~hOcAG-hLY+-L@j3MPSbQiTdpdL9XQkBKy1;TCS4!+)A zIjZ5KK0Vp!>9#_w)=D2L$KnD;i+|@>9l{dXSLLhV>_GSb&P-hV+*q1N@;>9{T;z>z z|IQ~|e&L5*JFc!5)EF&Qvos(|5M|(Ff-r`BPd}Tv_4-YF*!?}_6D#L7rY9yxbOV=< zFOaX6Pknw;%w8tVru`9Io+qAp++DM+cxyM&`4TxE?|Cf7nZHu%qQD#Ie4TaQT6OyM zy#~aR8@P3S_&1n|{byqKnW!ChY%a@c1$usu#DiZj{*Je$Up(7BV}++c`XXJK?~P4> zSbn}|?|tb(1?@+`QRou$-dHHMmP)WCj+e6q6N?ti!iS!IYB$d9HAlOTWyWVyvEAa= z?E&Zh&$$msq|DRCV{QS!5GXJ~nQFw+`{uPjM)EASZ~MoxFz2H%r)TnSV2(fK3ykZt zMhVJ&bJ&%82;>`vUmB;PFu!pz!J)1tMEapmT~;s{VfY(^6wc5vStGZYGiHbYXQWPH znM}%~6X;Q~(9Ig$qL!$S8p@(?PwoACw-{yb{Xf!pr5mL}GAIWCPoH)`1Fai?YnI_D zZ(TN`vpIVgh^2kiouX3Nsd~i}ohnlnOYx?iRw`VbONOd7>}&T%3+U8Y*X^}Li`LcX z+vE-%5&L&f%W!b1#~W#Z{$1YWEn$x@5EH$_9FBg%Mgy+8{AL%Hcum9Y!B$sS%9Eo2 zD^@#*s%8xDS{NR9p109DVtuzHL=aUnA^$l*wH>9o9mvQXOz(tr({?*b_=R#CLD(Js z#=;{Dq?v)E_D=lbAD^34Nl~8l=MJN3eC{8{F@1b{`m{=t!{s3ub{;NN*5s2~RPmy9 z5D?l(I`#H`yy_Vc2gH z=;)x`itr5FCOEe$-jL|Gg(;2u2wP)SPhOmL_ru%_H4+SxA3WPgx_-MhRS!PHj~|vX zL_f0|*48?~*}d95x4s6^>hlIp!EHPpH&o9w9ku&r?O+&CKke$ow$j$fnfSxutFCuk z8LEesHP`?ZHsINFDw5UpjG>G##(|LUb$9$hwPKX9f0H|0z~^|;9FKR=|VkOA4gY zTxi`Dl`GfnuY#G)Alr*5y+NZxB|CrbL_@LRr{ag>SLWI_Qjcna=BNNxNaO7kP~@m4i12r_yg z@-@NWdyH&xJ%8wqXYr_v|Ig==1}#Y^w7;zJvMw$=MY1vs<}3=nb12Mt9$rEs0?&O6HEI*KybP zW#_5uML^)=mO2lz0TnP1b&*e!kztT2qiCdbHU-v0f}*&_P(rJVaKsRxctFQmv{6Sc z{G?invM#bozW-rv1n+K+u=ax+m2G;hT~e)G+>x&1M9(oN;$3pBKaL|1UiIc)`8mCP zn8f`sdLUAMnu+k@*9d(In!|MzHd*kVX)5#0>*k3Uq<<8-&{l(xJU^ z1z33RyfKZGK@yloXx2h?dIo993$;U*2b@c+kae=eOYv8y9szlnVH6Iy%*7SDMojc@Hi_gZ|4* z1UL5OrJfzd+DbmWkIv{x%Bg`92C~Yv1Fn@Stm`~P5nkTS>#c^y8f=Wg9t%s1a`!hs z9G12yos3LbM&CrNIji4&HWV3Ket#7FTD&#KeJZtC;tZ2hLl5&6&QI_@m!4`NST<0b zrH_t!R0RXyEO1Z@F2#<9)~Yia&a6Xx)GPc!RVA>nld+e-!~~LuOQ}t z##<=%RwQ*G;9?+)>1|n~RZP~>z2%LgZZHjySH!GxzOd(RGV4w5qA7A%USdlY^BE8t zeAfV3U>d1_G|~_h>fcnE4p}0rxtBL}^}MXxAh<)<>a_4VbTb4b@S2|3s!C!g+^I8~2 zd|-nqw0eimUwrVNqYKMF(y$Lft|IDE_gpWh<^0Cavd>A#8-lOCIKYdTH0EbzIIs^w=+C_Uf{sA9~#9>h>z zD!-qpKXrzTN??OowX3n{X(oS8_w-*%BpWHnlsYz8&4x432blC|2s}J3dwcAZ#*&w@ z))o2&WWw6pZJuUvRzT&FrX&znF!OD`hClXxh*Q+GHdv>%nyWU54A!*5j z8u?8WT95ekw{YZSXk(di^_ktrzL}rg-FIbCS*?o(iRV89LaV%cxb1Vy-Ratd)Db%* z@aA19q37RQ({=VlB$kbO-RA;%J~bRR7#unuSnVre22L;l2aN%&O5=~Ozi~fZO;1t5G}-#|Y~iu!F0i?N)Q*SrS6JekTPF(ePTRV;-h1!l_4>eZ?Nxkt+8YGf z@Yl}z&{F%oose#Ui+-{`7er#WH#Fl!9&SjIvjwy)D^Z>ZH*WkiNfJNu6N^h*K|cH7 zL%$C~g6%)x1@C&~Cc#l?SLwLuQ5naC)}Y%9bwO;x)tc69LSh&xSIX|k(xmlOM^s5n z>DgP;p>0Ks#gcYUT}+%!&3;fq^uXv!bH}N@Q`3=2#Uw6GVQvn6v$kNjEnaf;rH;zd zQu%NyYaL<+QZaFiB_*ocL_IvklN6Z8M4ls) zXuJZ(jt-hpii~0+SpJ2m`N^+qUwjEGpk!}F%&?<+hgeUeMS7cp*b`q-8olDA{lc{U z@^Iepm3l+IHRAb>=x$Rbbr?JzA+ZGgP07JJ!a+ke)#i`#KXi_XC}*L|8~O$UM|xl9 zn{>kSV{v+lG<#s6GKkTi2*buZ3;sxoMq_iKw9f=x)RPZHoa~Hr3&tQ*0gG`;3%$t z=zqu5yDy9o26oIfytEznh7L2nM?uimjSs@IIpEJSsp=d=t#GiE;N}a#4-vL zn;f=&MkU&jfA)QW|L>xHuz0d(`Y&m3P7ek~^8YSy@*KzjjIsI&;n$bzkwF}cC`mty zlDRn=CmIGKsWdwz1pTBE<)Da$I@%BL)j<3$+w@jl ztJ<#Cx~Q_gua`ee>BlGu3AZ1+T>pIijq={#0|ms{WBcw@>Elx@Q`n13ami=a?b!ot z;?m}v4JXe4Gj^g|l7ItJW^eDH8if?gr1S@FqbzV{Y?s9Jx?@utDm(qdQc`}2XQdUd ztnhMQZ|J#Yue5Z@J9}p0DOd9{^{h@j;BlP13ahu7K%)R(2@4!(^ zBEt%hOp{q+I+b(jbpG{$T1h>c3-*+5YkC+64Vf88{kLpyILcvYI7{D+Q@iYynr;b! zy{ed8rkS+@ZW1f}8W*mVp2@~;SPjY~;^fz-Mt?HoO~fx@>DeAvz~izbNI2kS*0}d! zW;`qyC1qyBI*;bQK8WyZk!CYGYVKAaQi(M))FtgXDb?_c2|rqNOt9=t=W?kI-{SXc zL-7R^IrFFXFw=U^4jxK`jA-6^N3p#oLU0y73y2*L;Y;5NN3(lc;J5~2n*DT6J~5~0)79$@bldz;)f88F&4K|HH1T@3YIB0<{A{6>Wdg0ctMsEqZJE_rjl}1%Vdf^cx3oP0}%`T>{LsmlTeZF z1B<5GGZrK)>ev|LN34X}S-mjfk{Pv616~Xb1Nu1Mvn-yTEE(F?zvD->CCQ`EA^8Ac z``cqsgWv3kmi`-q{cBvVF4j#CTC6-%tk{sISW7uE1T{^*G%YOi81Z1k~@?SdW)9~O67%DsbS8#8a!qI+WX$|Pj5^*E!z2LS3rvCij zL1n1?O!d?$pYpp$)|`5o+`sl@t(#N==imk?CAYTBEdBZM8a8FIhOkT`;K2a}%ju-# z*rLa6jJQcBxovR$rSB%Gq|lxQ-Hj^p0(LWgUU~-jEsomIYMcq17(Wni#a8^>WibrQ zaG$QQ@fjy)QJ7J-IC@o&jBMRtinzh4YU(ChCI=DeXRHev84U`i$WU=MQw&AV>4OEI zIT&`pureC|d;Z93bu%dn)EoeDgRYI7rVf?zBkcJxx-G55@zwW6ppLrDdt6fM;1xRi zGbBBYEBoontY|3H+aFetLd?mS(cBKciSx3y163U>kuZ9=_^GVvBf!!wTE{UH(RKvA zMFDyHjV)V&w_TAOj}X~PcS+A}wg7ijRMWUyG9JQ6p@7z28&t8Og$ncxJfkAWlUwRy z);0*qnRhP7gn>6^tn+u*akZodaip zX1AM;=>|*(Ia+C0VktS4B|3A7AcB_UDAGeU84|h0Q%Xg+ieCT*{LtbrjgTW!Hq3Lv zC@|Q`SOG;1E_n5;v~lepL4Gx)d>@?fc=k`rR>j*s&f*7mo}#YZqeg(&YjzTIImv7O#M8{KcdHZ{hNXbHCipO0n`8 zKSmT{l>xf&x}^)z_d-RX0tGyy`Uk{MyIjZPZ1!Fq!Jojwo91rSgF`oF zAZx$AS-uW~Go;|JvT8g4W;aQ6Utz|Fj6(4o&uO+>Ucf*VjM{B(U zYgOXUPr1ug6Wud`ZY{{?gxS86T={b1lnBT_ILhLmg^p5zv&awRyarP9x5b|svmYdw zKQ*Cr3JgcgfRcs$$S_F=I9qNXQ7_RBEY2)vrE$RefeRmB9>}!``=ftcY!?c^)nNSqH3Nq&4rB`9-gM2a!no>MhxWdV3)CrocW|!aM z{gQ>>l4pU_z5>mKw|dhUF}5|Uj#sPkI^a2Lmic}Ua#e2evj@hg@?GI9{f)}pFR{q0 zpMJtY;Oa;27wOihpDS%z45@&qXb>pqUYdWsKp*>wR-a6#;9c)Gy*gk1r*P? zUKL{Z7T@we+h)HGJsa8<>fYS~()X6${QrSTSda2bl`Fl9E%WjM6jfhbU(}Tef=?N< z#A1P;Ml3NZJ??EKQiYuS0=s3*k-r5|Z~Uu@8_btAd4bWj%TFFhseOL0eko9P4k{p7 z%5^jDxGV#qEGw_}U)8ZzhOYqlfVc{*z6Fu}j<NfikGx&{G2@w@K+N$Y znVx}}>a{!efZ?@VPon+%DyEiFB_XG4rkqH7VuyCBMK>9(od{kS%V95Ik(v_B%@RUX z&*5PR6a2To;E+Tg8L8aTi(p)6xH=ZSZH?r))iNM}vLd>j9Vbm)C8I%Mb|>Ku(#o*6 zdkf(bmu_XtLWsVp;schNtDd#^ro;KlrYS2`0iG!_Q{PAonn`FTH*Pl3^M_)W;1V|;|M3Ng#@*7S|CSF$h z0i{SNZ-%S7468xHjuPFbB)Ku^4EcBZE2Zdyp+!;QxwEl@Oy;W~N2eFnLmO#JfkSMK z%fDwGo7Sgg(^!Jr#1cn2LFXeU&Cxm@ulALVz}8;NG-3DMuR&POMOhDH>ze|YmTSn{*SA7 zimtTzy1v7XJGO1xwr$(oakA5~wWChQ?${l6Y}>Z&r1R!`zN7y=#&uR_HEOK7)^E<5 z3)^lACc7{*Zh2Q`z7jT*6iP0`p{sVqj#9|)s3wKj4ki5q>hOEjpIqmFjc?OO6D1Vc zOGH!ZFby2q0W4%{ZjKTioF_JR#r|Y&NePqr?olW0&!Yubs__P;K-S5m_NG%;dM&}~ z;bR!{E2}oxi-c={5DCvr0`qa+d9c#$;^95MgtABC-;h^qj7yTLJR6u+glMnnpbPlE z0UPN={0p9}`#BW8Zyd3utQ_ElQTOJR7|Sdt)c5LSoM@RkaiQf5oM>$X86UVG1ngoH zO1H`3fkvkMgiSaT!0yxFH(Ri8A#&M5*8B%4ZAJJ7r-S~$+;&j-Jl6BNkhZF+E6hRj z+;oJ^vS8=&I7|X`kO7olj4g%Yw*z&I>=*|JTE3$h%6kNr{Q_POTElNNMwz1N;(xzq zG3L=9)46b=`8(xT)Y%in71*=5&DFT9MuC=g5v$9%g#DQgfgL)nEY{CTw~UuXs7+yn z9QCWLW4tybg2%Lg1HOjpQq=(=ZT`5-gK>b-(t~Q!)WI+`sPK82Y9m&OE0u=Lv`%@UtOl|u=XBjXlq=LRg`V`7ji4vg z4Q4?DjhJHwe?oy=Xq<_P(iRx0<=hAdC{U3*Svo^0>OrgxPAu4ddw#+LA=DkiF;Cx? z{&*xGXCoY4sT+3o!ULwC4_W7`e>g6X$gyxsM9~WzsKnaZRy$EOvw^o!IO7?2X&HA( zj(5k2)GJc>_5!9jZ!?5F-xZ;>`kUO;Tp2P$#~J{xu~W{pDT$7IO0X#lvU9)9YNzd5 zRMltzZKH(w)#-)0xCNNCwmj9FV=GP;gii3zIXIE8YSpfM;}?d(@?uKu<8BWq#QpR)TaA>1pZQ&_eScn*E`93ug|CLYM8X! z7EGk2DlfGws%v*2dE3~oVX(&x&XV0FeNqPn{1j!5*I$%v*ufc+E6QjHD=DO}V%lUM zJIOd04YS{n`L}DZ)Ts7nYD3BWPomMj`&GeSz0Cp@_WEvWfm2nSD%hW(zKZ-Ndm7|2 zU@6!IL9B9+EOntIk--tw!HsxX;^WL-o)=HnA)I9|dHw}8s~emvt3Bcgvc!|$yPE9qoLff)P2yxvkNhdHx>44 zsmG(rNYJSQ%+hu9F_t!GYsC!Y+m~`)+!N}K^Ad(!Y%^k80Z!lp-?8EA z_mPBu0H~L-V{#44Ivm*A!Uy7g3S$yWrbUA$SV(UY)lJ^pM3D>xI`yuN9cHFX_ zK|b)wK)lG@A2EUmhUe15{@-~|a5KLl!A7wMIYD@S#B742LvDK{yA$xRCv6r#VL&#h z`(`L!(F?*tevI5;B26CyA&EYqgDgh2D9~D8Wl6BD+ZsU%BeQYM(A=`&Hfe~c-6Y+S zc4rQMbi2aB;oK!}QK9@UFoL-Swq#9P)QXyi^kAAirgdrb{gr-3zpfKvp#Q0FRs*i} zFyuCS*)JUgb1;F3IB$I(onSzvQ%)}v7%&E3!ugt+xY7PM@4S-9W-WC!I04e`Hv*pf10Nj5NTsT{?k!Qq zFy4}x(zr)23AJ3!X|Gv|K~qE4(sWRPnWl^5^6G;Sq8oWHXd(OI-2Wfzsxs-ul*-Kq zvgTEK;>3>>RtOt8G;|(>FrP8q?#ONO(~IfkN>~^OZj*;Ajrhl`C3uZhLnW>- zyp6-FsFi-A8A7usKG-n)cw`xc36kGL$JecKfgI7R%HLd#P&$Ib<(N&XeL`^e zWWd>4Tw%Dx%Fs#sV^kzo%HRps{X63C>aiv-=9g(|CK=a?sap1mCB9uH?X1|a;wUS? z;cnOwqqrrIZ1p&llo(@Z8g|~{52R)xc{+dnFGn)(-+83k@2IYj*cadKB%mSE(v#B*ri5XVIR`cJG7-xdEu3@_%Ze+CMH ztEcYTG;>=hker#DmMcoH>IIETrb!N&o_?@1kp1?;-F5?4umSmou<{nzs8oKKsFlfb zM7YjJJ|I1;Yb~|Z!6?jq|A_!&cEh*uY3lwiG6P{r3HS)>&=?h!f``{w7hk%p5|Ad^ zxSuLe;ZVPuGk5w9?k5~6E2+fRqEN1RN>rUJsXlw`cuG<}ft+=0_T|VJn@h7flhW z$_ACwyYWUE_u4Puf8GKih1v>()q{h9X+eU4@%-PZlvhLoDalg+TNYX#v@R(1h8HDeN$macX#TL;Cd=7vy zK_?z!2@WF@V8}eK;@y0C-Y!(Q#xDoumT{RDtfC!}Y3cgsp|vhJU(hRF->NU@z9{Ha zTeabGH`!3I{hclVZlv-3qq?2jP?FABK>Lk*sANhu&rcnBlU%}~R9M**T3ae#oX|`|3-04c^<_q7m>@R}*VBJV z2;IWsvi#>fQeCQ{8_?EHHg%(=I5_fDS#fsD3I`*>g~%iVMBM|2V1UNyvjHP66iS2p z#r!nvRGBJKH1TX-MR~@XufkfvfNb7}_K8dM$qpSl2q~S<3Y8)^Sa5MobrU@WHyPOb zW9-S}{x?jt(~g9_AV$Fh(Yk##e?}vLl@zObjB3|49-N#3EjzwB>j1HXMI!+|E})Pz zWaY2cIQNa92--%T%R~BlgQO!|$j}cVQqxP_+BXdU7sTbB4#BNj{p+JA~Vod9DfN@5HeXR65Jg3Jcu-vRWTH*%oG0)XOw>PvK&f_1Fa_- zTV5TGer(FjaQLO|SF}@Lq~T`v z%MozX8@*e1JF9>8?E8UxGnx zvdEnZQ-8&jE~Y;2{%LN>S1p{aw3io_zvzQr^pSP2PyCq2Fo5<%%EXRQ<+*(L}qQ(>W!OZ*V9&0I0 zUAQibiuISdOv|k)FsW^pX9ESMrA%j&uQ;8nc_oT@g*PA5!JZV>Ja2n-XwDXAtj;BW zmc|WM7A3trt{qijHql(7R#uJZ;qfwUVarH5%F|8(_I9NvLdD}LC4T*DoD&3EbdvwW0;)1XWHpxJAewQyCs6JGP#aa>Ai(cDrEw#Cur z6Nh3pKVx;_mbL>K0GA=5cHZ<*wV+lh*HUZ&ajwcp7)ftOVa>+yvE&~W@1zX-wPl)h zjuI}yKDyQ#B2O+3JKAqsf39Z7!UiZeq;Jmz{djgBG` zvG%?3U%x>A`ph-D(7|QdkteZ7A2UgRRelDfa*|PV#O{hB5r26Fgcn(Ue0Vqx%qs!7(c#`(?a9Dn8*MW}K9iaN|)sjIP;h`=r2-bj+-P zCjgL{%SiP`ABjEVz;H@IC~wybFEBxvMGEgtRS-=HT;1;mlbB#GT#`dmEJ%-`W?KLghQ890&Pcp zbU59m&GRpB1I(8A5J%NM{AJUBAAv(>I#$3K{E^J&=q~%`2nrJn=Xx^AyCd_;>I~iJ z)GB)m5nthK(n`iw()41{a!%vS}v z+H575s(W19w<-p0Zgxxve92F--F zpC-1WDsFO!k^}4y)(>M9iZhAd|-3-CKLdgn@Xz*3lM&1kl@vE2eN0VYy z-JjhYcv@N~n_xXqD|vUSS6B)E_n+(Y;*E%U8ww1p7G$Ww4^-F2_{Sy7LrNeaGOwc% zt<}p|hh0G_qmOAv10_cj*|{vvLnYnEdomIw;`|s`qntgt9Pt-$u29PUNcr8eM98y5 zc!aNMODKvIZieq?X4LZqfuSW{??! zC@G3{(!|oIV{DJuV~yBY(IvY$^eIR} z>nZ75P0lD-8;$J)ehi}aab&52E0;MNb^ERAl{5opfh-T+ftIp=jv9+&nX+|d8QE)A z!VS_>jHn&=p}tWw@?*-bthz}hRIciARjeiT`%cbiTRFra3{AURbAqgXk$Mz)lrGY_ zuxKDI)spQ=NhLgDruz&HOO!M+JiQ=+mZqC9f0$r$Sy|mF%}8XJm}Fb}Irus-{4dU^@h!}KO zt8dqyYg@^#YqVT#`;)vDkW`nYku-!!tEE|KYWVFwKHBRhC-_OzKd55ga3Es1{27)Q z*x>7C8Qcpmq4QyerVb zWrqCPqTO$F2Ozcsxzq`7ZCsW$yuH12GnQ;p_a>wIL3=8!F`M~;g>o`CYZM{8BlGXS zj^%1X1)R^b?twU)S2~iI2=tAFG7`XSAS~6{^zPu+@MZEeq0h(YkeFs;v_1A7_vrpb zb+9%EK4sOx?knDm(HvoUAp>`1b`$IELfy!kl2eb%tV6_uw?!r%d)v=sFe}EvrQ9** z1GzVT@w&E9Z#A}O@=tLOUN&TQ2A6{G?@0vPt_1teB;9R09T9t^)p#iT+QXirK%dY| zSm~abB_P7C7xt|D$}wFCtZKO=*z5FE9ULSMMf9%$<7+gy_vWbZ0sU)*N9^qn)W1J| zeWjYr@HD|$CgptG^yEWiJ|nFM@tvoF@}3@%JchXQnVbs3jtMWSpwEk9njuyrobvY` zO~Bi^rfE@#SG%2lobmLH5dAQ922SgDZ+@-3Rh!p#cvl8hn;K6KttcN8z`h#H1?q9T zbcclDKXH9?>J=CAMYY;CB>)VO#GB(xbi{Y~6A-4n?F@l8CJ%A3;ZOhtEvnLU!DrD9 zy|WrjXVEBG5pc}@AnwuOc1qOLc?-!QZ{pv_ zNR6PGUCOs5p-V|Hvi?GT(n4FeT^7jpEDPgwi7W3p?HU(EG+Z(~68~@ia4C-ulk|lt zUcM3{Kn29uAjJ_pU>wL%8C?`*=yT2bR5TY1T(T;s}KRklCOrOjS z(NoO2Moa6|1f)}1vX0U&X^5wd-(P!fzP}ruI8t^)htzW}K!&a5l}-QA#EYt$pE(1G z$bNbya~!7{?wM{|{By!)IA@9r3FD))Zk_JEiSK5);UI5}N2;8a_=_4A3HbxV@MYh( z#M7Ud7dp=T)JS6i&Rlvu8yAobP4;bE!Z8-(!u5jTZdDgtaV7Rd%?x`D_BlCv)pgjf z56yQoPM!uJU;~O%iR}x10P|GZ=hVg;KTb>m%{!fwEu)2F z7wy|v7Jkx)M+X01d>uK^Y2V6`j9+I&E!uZ%OxLnM{Fgr-dJo%kH5k2rbV>Em4BFH% z|Iy~QTlmQK1^9g~EdDAFN43BSpZbgPnd*&KKWsDX4piTk>Z=VWaL)Res*%$+WN@u; zbs&7E3H~Gvf+F)LlSB>l+ivJR3Y|hQhJ9zjIiluQDis`MznyzUW)UPlAuB&JH|!7L zLG2%L{_)PX;@UqFPbQ_B$)>BI#t{ZLYPUl&s*q&e5?ry179vnH@Ngv)AV{2s5{%&b zZXgh64;1SV+d$o{ngS%XiO9mNkl8wYLm%v8V*MelcdZqE){+kvHA=!Rmq-?;9VB6y z$Yy&FPV(M&9pg+Zy301)fwtd_N5K@rkX*tBMnNxPl|mxUHk^#MACG4t^UX&glNwUQ z4Z=bTE;&&&hh9|9E-@6bUM{l% z{~lbO=`%@YI5052FK-_WNK}^)R5S7&n5z8(oIAw*q^l#Bjs*J-jwWU=24>C=MWa$Z zn}j0OOkk8Hg5FjfT}1&gCdKj?vUTROdF3u#^S<7d8;I47^)BCUFWJEqDovrW^FX#qLK-=T2Gx-SRNkc6 zognRy?ANF4Kypr3iD5^>+HQ>WlF4otLOdcWLU5F5WhMgil*fD@5-h0^-@E7Z+7JtT|zqYmdj1A?##;D-w ztGMfUb%4z^e_&?mlSFM0m|}ea#zxAcHSgUi1nTt){O!)0lJUk$r}RfPhTj>3zd-M9^9OeFR^U%#KXE zQb3pY8!^w^xO^-`MM>cjU*MRunufgO`z8KHz2lpWU72?GZPMkwja@Aem{)-|BmO%G zbQ_+djij!Q0Fky*j;+YJmg6xc`_YEB1kNb`y0ns&xjE^#TPAzf>gp)g9Zs~7VUx>} zqM+p{Y)a$PwiLLW!_Sygke`v6Qz-wi*Cy%~UvBH(Wi?%fKL^n)HP+)Cz6?-P#6H3M z0nPV-Xxm&SJ@z|b;5X|65JzKBXUQcYN2@*pLwv8(eb|%VZKSj)0y3kL#lv(|V|K_l zwZpx}Dj{udyc1$k+ven|4JiT&)}yKi^z~k7&TjKYq&sVV_@?TQS%KjX4WE=9jEau@ zLrn3F8cdpz!DvJt3mXV5Y3}7-xAGMsj_HXW+G9y_L-;jLcuOH4VDCaH$u7M`T|ONy zf+jIry$5v#{m&w5c`?2CTRqJZgN9+4jaL7e#ss*>3bMacyYQ0cVw|0L$-W+HX)qAG zGoRG95({pyy6zOd`>%R;;-`nJV^#sSgl$gAfpUMfP1`a0G?3^Wzy~r&?9T!$645e| zX!BmIAfOot?|QmKfvL~sV&L^F-%r&o?jYQ9(GlrV0W)W*YulwY=NQ)>xxF;}{+Anc zYEp#S(JS_G4o8fnanE95h!6kB76QWUEF?s(dBmez}!q$YW5ZZ@c89Dz6u-gKVI?Ivw4c@#Qy^2TA@a6?{Zz18ujqO=Sk%pQ#>hk|!Vs zvVJT6cKQt`!^0;jx1HpwMpYl^Xc|CKILB4`3-y6Qb4rTt7n;^54mZ;+DAsvrjp3Sq zKHjM=$f5(5In5P}ImH!agKbcEBi&SE(CbDp8FZszAEBo5<^2#&bVWlReI}Ks+moR? zEQ%#&9ZLAQ4BVzHp~Ce2r8Yo|D_qA)lzgLZ1tOllrG*c*lYhol2O3G${s8ub8nfkZ zmI9N}`5(-<%z2~RHTFXjl#v2PZ%?Ei=W8s=9L{QRpSho_Vz+KipKyweQfcjrb+x?p z25A+H$FNJC^`Lw;I(hd`fDA{-Y+rCel|1FfS}B?VC?)wWIcR*tjS5V^L4H^7{wpMC zcHqAsYNaF<|9j5!8o`IND$d@Hs^Gw1u`P`%1)IJzy12x>#BFvj*Ejdk;Bc%*R$SP$ z%Ykw;5V?1Ev<#LB_|c2Q z0l2jvnYpCK@5(huvapxw>vz`6<<(kUKaJ=Ga6JQLy0Fvl z=`zv)Re!gzF~iL`KfH8AK9lTbB0F1tc{1l2Vsgy{=2~}UJrd@8$gO($KK_{r?6Tex zdt;gkWb9pUwk15AbexzUvo(^b_nVf_3TQl&K&lEF^nvZWE5nNqt2jc*kaqF@&gqT< zv}R}t#B&@3$js8LrC*~Vv|&LqZ9_@Kq(x4U-ScRT7kctAKyRLj`$a+OIJNgl$`aEFy1~ zJfVGQ^47ka{Z%T2r&I zuSgT9s@*nKCJjVB7*>xhzT(~(R;_o76T_>9J!XaXKem;lu#$5dDcRBrwNd`;3DFc` z+oK0x3+PG2k}9@?Wm`q+12|iOOMD@i!7ko&3|z)x7zoapeQAAnE#}L5PT76UqvNNP zn6E0z@F{HeO@+ZA-3OwdXuxY8wp?4wgn{_Ghssg(* zaY$CK#3-cdcdBw?r4>DRSwdMv(D=8^U#w;)Xm$7c2Nt|FJUsJ$w9r&QmJ=j($cU(~ z)bu?T!YWryAt~WkUl%)9gb4@JVWlC%)F%JjAq3JP1c+kI$=0pQb>!QR(J}O#G0~^` z_>R3A23(B&XKII&b}#7F-MAD&zsYtPPnGzj+y))7U|iRVfU$`smdny{^h!E&!{kj)(Fg zRz+?fs}~|)#^Qji7a9GQt=Hg(wi;V!^l_;`paTG4`~Q6&JE9Bp$5?N5QBIqRTyDv- zWr0MdsK_w(QD-S<6BGd5zY7kW4eh~7?D*eZK8)=zFav%1_jatb)2*bj!E2hIjqi*> z$d*}93+uwwpc+dV;Mpo!b5o$Wa7uz2opK2Z+N=!+X%*^h|B$KflA}pFqIe;&cdLMz zW2T>YJArMJFi|C~@!s@klh^O6)%|dDLl_AB_f17=MFfJM&l5?a)?8%huGJA&bDqsiEubH}3F+Y47Hqea_xRAJU{=kmnZ_CQbj#z{B-O(vW z!qU^K?09l3TrR9IEi;f&92f>A1N*^GX7QW1F|)A(WNBgkf>2AmFX0-o*aD%hlfOU! zV}dDL@5M|?TOZ5!N6H6$gS#GYWx_bpzeD7n$CyBL3>$&G|4}Lun5(5jiZCi~lKqHZ zt`?Z3$p0I|`|LiP_DV<)HCkyGoJh^dCBAWYIV;PY;L`^0Gh;V5u?F=PV=CpIL(rT# zgwgWP=YJ%}n2-loyD!3qg8&k-5eIH)<7=XPZdicAEDO5Da(}o-JH#{gp`NP((YlzaKcBov<~& z{`1$?=gR>%*wv0coOlbRnQAYI)19eoFQGrh4%4jv8>{gKD6ywL6tnWt1PPdXzpZZZ z;?jkXD|>C)iGVMN(|zRMFcJYnM&$M)P4*Y1ZU7}~4r$}3_4Uy18Dek2&n28!wzmAL zlqdI0fbj@s6xw7)xtf7CL-y5~M9<2MW4Jx;993Fmyeevv12jeT@x}PiW`vKm zn~MaJ%yvHEGhEiqb|-+ijdELuv#@%}#KJm|$*;UB+BC%$Z<1h2uL+hLx4>Plvl6Af zYEp71)vBR z+Pk7_gGJSO!DN?*$mitIP!$GEjjC|{e)CZuEoPkPy~Fo*?Rd8clU-|%9jBongOP>x z>Jq*NSLP_vPvfSg>RsUBWN12hTD{eZS`$CtltDqmYtCVtMMu18_&<=ZWk(b%?j8=h zsDd*&wODFW1eJMku0tV-Sw!wJnWyD>K8=}iI5^0SsW7z0LT8)%vjh>w^4l;^_YxZ& z&wd}bhqm5(D(Vo1VFx>a-t7&OOwsbFim@h|Y6iMOWpfF!)D@V3J7~1%Q}!@Og_XYDS#`(LMY4ahQ|A;5-(z3fIZvWL?Y? zYCZwTopCgZ=&ZsBXX;FP=)wANs3f1SH$xZy_9~S3H1EP)#cWbIFS#*npSs`}F2i3l z_fAOzdVa{zZUgo?%{r#P6jKEd0Qfc@DdYQBXTn@x^;x0*q4sz$%sqd*P~%W{Bx89% zUruPeHVqxjlaDpQ=x{RpmNf(|g5quOa&+@5&||&pg8%{)Q|>;_`Lm)vWByhA)WH0A zULpcYtRkz;sw8X4Dyq_vTpC2EFooraN10ZA&5=%Fa05b_b2@TDUDC-3OZf-P|1hJN z@V+Bqa{A!aMV{?Q-S(_Ft`_4HL+%?bCLc!JGLOPK!pZ6J634;n{Zb$%pvZm@qsYxR zf5MUqbLE(Xdj6Bii@vT!frCw`@U9BHe?Qt+uP13KV^)z7orzSVD4~TI_O0 zBr{kK@d1i)#6bmsLYboJTO|CzN!JT8I`&@wEL=MyT+MDN99A=-*C1=-`)sG+78tnS zk-vu}MxFu5DR%=NVZ#3v72-2-gzJvC=69}iiLe~n4)nQ)#k#uBYLLBrJ=&AV$w~J7 z{rxmd$=L7==6O0B`ycfGLW2Ovs!1QvmV*XxT??KJzIVGf?Q+gGL&^$mWW22ApnSU* z<~qNhf>E;(*2)#+=XT|#Jp49vh9AVvAx~p67V3x%z(#^N%0kF_gguS;Cm2v{r zc4;`uQ)1>of=xGWMm1Y;6eL(=5Y6-hzkwn@zxbb<=(N%9#V@uDniq>h{l0-9t0=04 zbS;#WS2lI<_QOHvGiLMBubUM$9u6~jQrJkc_KowC37s}!WfzTc|A9Fp6_OR*!vnB5 z=c9C`)Xom=Q};yAmsYttpUSVZP&%ksDR*X{wwo6Vm|&?K%>oo!?>}5N^AD--2lhUC z%>4}`93RDE@%qkQO2<_>ptxrE2KB!?*|4?yiu+|GvHoI*w4fPJLQuzFWKg*i22g;j zm_$+)mWIN6tK_Vna%0$2{dG;?8S-6qmn#fS0p3u@^KjztQ~1x^tr};rCmye9fQy_< zvI1iZS7;z}Rz38cdP(S~S&P+P3Jm2rK;R$>u7ghESbz!rpRGm7;rB^cCyFpLs--L! zZE67;)1(=-C=;B1*Xejilydp`kXPB$`28Q{H?^K}s%dAT!Xxvi#wq(!XOu1h?PjPt|Rcv%!Q@!?zl4ZIK>Z zbM0Fnmz`SylUHBd6(Q*8MiCa@)o3sb#h+&@7|FW|8rxM?r`&ryB1w!75r|Y1BP}&3 zhQr4+)n6G{?r)5Vz#bX=Y!)T0Do_n~b++p&KKM1-O(xOZM}Eo@ z0c~}VsbXo~VrDjOKg9}3&gSPVyY2#AW@hL`s&Ryzy!eDwvU=KYYxa=7dle0Kb%Xi( z#rp|?Y%M>fI2^nDZgTsICt&z)FMHM^Jgxel5t+#OUU}G)6%ALTrj}%qi6rB}am>4h zwkDGNu1w>}aSHa5Os+wqNZFQU1I)gr?FLwq@VA9eZPX=!OFII|6HoD8IR zccH{h9O~KJjXj&VaQ~7F^w5jR#wA=`jo3>ExJ7x=)#7j$<_A_+5P|zy9{#=wzgi}y zzcsm0(LPr}XD@6$di16?x%v{ ziq^4jfxs$cQ?l-ii?o^MDf+=O`WvxMbA73sBzrO8(trtI5ty~gsDgRkt3MyW+bi2t z+f4({W!ez?2eTvO0=_v)x@Nhxc-ydJNL2k=Xp-jnD}lwP#wk2D0mDC5dAW`4KEo(4 zumEX4&jcn%l_g#olLEzDbt?6$4FTS0Fe5m-82P|2mDp)7G%sG+q|=^s;9XUeLQ*?Ppug zv=d9YuzeR@M2&>(Isg;i#Da4|gs^V4L2Nhx5K%aTk^DQA-qH0Dk?|KQnw3)_8dty6 zzcE%3yeyneIHS1ueZ&xrP!woOnfSA!`TLe%e z*s)>5Upn-5{mS(p<@tx_<%JmWQhVzwq~i76huS-8(X;vUq>$JgW) zXkO@zp}b9!3(T2qD8+m9wZuF1^IKZpGmGETNbXbAp>*<;BkXN!R0YlIbeK{l_0~oB$-0!Yo@jjkoimZLP7H)(^;boX)&wzo$@4uAcx>lkLA$a>c`*?PP zvJUmHL3?nj6yS3feWB-HGX{nn%O+=8xsg_E^?_g-Uj*WEw8_CHdg$@+BE^?FDb0z*?~;E=={c!8d#M6s#mD0H>Yc*M<|Bq++sNP2H2i< z*gp&+!7YZTo)8H3S#7r~i=m8DAI%*BNmhZm{j?FoqBSI|_HqJ~+vrG8q$OH1w;Yo^ zCPP{AO%3tjlRI9CA6`4oZAVnHS>VWGEbwj zJqCDNc*%K74wNFGc;=R z2UvhW($Vyo<*4@bFe+U#B@A$urc_{)iL{!v*hSrs=KdEND4D5>JfVvuv+#-NOdQ*pduScP~r9tr+ zjAgRT-aY-c?7Z(>W3*CF4VJDx@SUKsQBIT?d$8JVC|6;T$EukwNuCDZpo- z?R1l)lI=p2KvUr8#V*Os0MX5GWbAx}GM0)BM88`h$g$)*I?35F{Vuz$FT%DIlv)Kv zm*c1-Hi$ngd@iu8w8D2TU(;%kQcy-x}4ySd?-y_ZCeQ{9Hk!=0d5#}QoEcrFw}ZH%E`c`H;f6qL22C7s9T*HE79V(CY>Zasx7`lgze*J| zU??i0ySdBd1}l#zl`ElXHWjY1{Z?5T`k_$$};fNb-Pn)j*DDciQ2=f~Yka zr(tC{iTCD3uM3egbI*si+OCIwM6g=tcF(cefq(lIDyP|JT7hu8Czz^Tp=KE zAo?0OBP;a^Y>6BTYAqa*5D2%om=Fs{&{~cf>?AgQLu~&<$aH*;Pb=<5_P7$&xl1Zq z81tNIZh&!(JG9yMu1@0Dd zZ;(pvV$SZ8*gG|8boN77baf!6JEXd2Q9b|g!SR1 z^I^~x1P-e^0SBvCi-bq!xaTyeGJ$6)YO^*5hYVhuek0Nh;++v|05X|BQV0q-AbGRw z)JHZ$+5U|EzklJ62ga5lL*GE)h7|lD$BE$4x;yYF%Wul@{~xm{kj7D#F`qc7!7SSr zZHpv3ZpXq}T2N+|L#pn224E_u)Ym~Qq+F`_$(f){I9AEi{F35_RMF;k+|ZxcW{$Ad zHO3-@dnKf4B~#s^8GKY^>iFlcqtDcHvv^uhvi^?JPrv4Qar4=&Iy`*D6+h;HF~8fv zKZc|WnxqWQ13PiylI1HzAP+xgM%Iz(qi9&M0nYFrCF`+rAfbiYg|Kx>Eq_!(ht^N0 z4B#N9ef6`t;$aP9T03akj~3Wxt%dbd@|*5UTaqL*2POhyOx-=wjb6)@MEV>)ubtdr z%kNz+teBPw7WV=oiwvQWNA~gD4*uhBLsxWt5<^i$0HZJ-;c(;)-w$C9g+|Oz&W-pM zj}V`xSCgqH@1Okbi>Smu*x>I66vUf9^Ui6Lee;He$mNM`6J*qF#*cyLV9`$gqGq`o z&>#1g(!p9n(#UI}gUyNLYc6wyK3NbDvSzq~rK}iY*fuFfa=Yd+slgqg%w&kg11iIA zR3@?8{=?o1^fY{KTS@C2_fs;%h<-v%;Pl|N(;klC?xZSm*fMG|e0HDT+Rg7*KDj$yyOd_zg*6 zMb9W4AFW8KyPByW@O(S*`fERMa1aN?F3`n)&mD^AL*wCsHKDsWFYD~z-!xsl5$9Z( z*bQ=Ty?`5ioZ74JZgXC^{hSpA>{+|vaaNs`YkwBp;uv;1ihnn?mpfj&j?uWaASP*k z{Ivw0Eg`n=VEj7JQuuaIVte|ir|8Jx;ii8LjeCX{>4`W0XxN@O*NPo3I>x|Pet|*6 z8*VAr(#Nv@&}qPNqEcD)`(6COkJC_n!mjOjIb3%FpjYyd@6}I*2)7ik)rY;(xpxaTR9oP6OIg5;nr)>@kQXrRd? zd+sypj^S`I6jF%mt#;0(SuKVT?Z0sKVv+J^#C$@;&UML_e(fTu0qvYC0RZ>FPR@=Ob zlnMhr7kX;-uDifW?E2fcL*HWlPBr+qECl=zU>R$P+RGU;GK0fD7RXp@KV}>DORwF| zQCW}kz-mV=@vqWID%fZW*A5jA45(!c3FKfl2hqHf&2M5s`%72R zjs6$k^9OWvt9>z^+pnS_^?wE9ae@+9tgh%hCxQ}i=HCMM9my6yPeo5I|D`}_6g*!8 z69tY|4PNHA?-s>c?LbpSllqPJGjgj7EgDS!PhcnwaYM7Fh8ipy%+>hhUmteoj<=V$ zhy*aM8xjO`uZD~ndHARMdZ&V-K1*~>bWZdq+*$qE)E+MJ0*y^`ndXikCK(q%8EpY# z!pG3n1HlNgu=u5;Ng{2vt34AoZ_0r11FR>>wTG~?F%hB|r@SJ52cHOOgPU%3M6ESI zm>uu>6Evm3g}0pzGA1!k?y^rl`Kua-%;6kCVV~}<(MZxv5p%TMVbO$8*vVSyA}0kO%c_l zq198hE7T2F^YnZFD~AW~Hhu(z?!DoU)?2Um%l6YLVhViP{=4 z6W4T=TMHaus}sR)qH)fYi>E=qL+=H9XK24mP>nflyf8~gb(Ev@O6c?&=9)~JQK2SG zre1IfeR6Fe_C4R(YX;hWkTQ0J;2L^m5R+EoOM||u)(~PAjU)d+&7OkgvLn^mcCM3V z=@_|vg1G-7&EIpISQvg5JaA;w6FSH!jRs^r1ps{sMgkU|d?}hSj_NC|vYSr|M2cM0 zQKq+2st_LtA;X^!a4nva(?R3jxW|rHS5_XtUT9Wx6ucM)_PYqYArQxSUrtc!VMv`y zc6Wdyo7y|KJMTABUY_o84InLtN6`q-Sn@t0kwv#tQHsFl=Fst>W_Ydii6(?W9&%Hr zenOUMr2z&b8HRvK{YjW&QNTq}hgJpdwF1#h+Kv!4_W6yj%WVSS&lz2rTu-vKacwSCLXZLTXjtvHB5n zHz_-LUhjIY{Pp+eNw+@`MBP+y=&IA2JLjGi$|dp_g+1UZT%O$8i_a%$?gb1llD_5f z69L}|FCAIrm|@*e)vg?RF}mx<*6p23I9qd( zB+;!RcZx}#j)L0yk2r>Rc6SZUyxe)b73x5rJ<5&bs6UaLNHg^UsM>CF-9|(sQ84U(OFH&l$F0 zsuvd5++;QuYzJA129;YZBduvq_$>22yX3!LvR|tl9BFvv>AC1+-bVpoX{}n(uJzl}}Ei>wENq9IfN86b_`M;+uA)#jwJ?4_iE&BO= z3ea)Wg^ga~0l z6^q@fbv_V`Xf&MJzmCuhmou+vm{{21%bK^{!aDCHK$=JsAw`!ZWCez?Cu#T;*A`v> z2oe#F=(`0~;2mrnTdGfWPGyyCwsE)#cof@9)g+8t>U|j>A8QTymH@M%Y;{Ecv=$4)5INVW<@)aC>4rCeh0Vjy=)py*bJ%GKZ>-B6FxYYH?)B?KRTSYcTQ|h1)NEiu zVcm?NI2Q%!Rw(8B`1{K}5hpyNJ@{{f&ld>vf~RSiVs!+Y-+}9Qjqh^}M~!uAJ`LQv zQ_LRohU?Vh0>lT2)#?tM)DX{lGcmPVvyFtFkJV@n+0phe_xKRSeSZ)n3yOE`p6co6 zp~Yu&bTzrBr_mjMerCnYFfIn=A&7r6h~F6wqG3@9X4-!vG--!(1L@vE)0d6N@?X{zXu^0R9`|I&h8<|jxA&N@i9!eOSLpn6 zJ)UD%Sp0{iuaUZNyZ30WGom!%@>)RkSFn*TWbTIO_P_!8;8W?o+U3brio|E)ueCp& z&U=2g;(86xsm9&Mdxp~v@V6CbSjsq2(lpy#lbB$rtdhn zlz_*b^<{GU^4n`Q#wL7UBV8G1KqUPV?sr=Q5kFMkzhk`klEEc)ig)0Vz9_OnuJV za`ZA~vIb`Hyuz|ZVG|)>xl$_2C2&?PWbmn^DqB!dCGDqv-Ny+!wDn1**h@9R^WGh2 zX>*m={@H55j;9#HdQd(I)LVANKP->2O(aNIwrglK`&^`6vidz=zbP31K|cEnqiQ0y ztk#xbXo#-xmlr-!+r=_fp`bBGyOK7mF-_n3V$g1R!{E6aGgDDm7~5r)_(_aFi|#VU zR|qeI4}Il0?H04MAem9l$6?9s$Tfp|)fg?80i)7|_(!4RM4x+BNhnjmtoe=9LfJ1}f5Dh_WJAyS<(c+p zwU@zgU)qY#Ax1Zx%*fu8tMQ)?O=OwIjuA5O7g3>tMoQN^Ac-uH84V|T$S?3(gU4Z$ zS&8B54xiU4LQnK{fD^3iK2Z47-&%r;1pX3J~j<+q$w0XPNT1d5)8^u zz_2*6(fFZ=>aWne|2;tXcmfbic_zRs$;>ss*3#uzMk z%Etr}pT+@JsoN>eD`ELxXe?DR*ppA-`u4J!+V&XA#Nirz2`5wFOw$hBzG&uS_$+JB zKby11aur3wiO!Yq&wBHai)?TFBvyoh_XY1!+q8@Kbldr~!8QTo_4*LXSvq&N-l@=A=5v6zVx?-vn$`XGu%$jbmQTSGI)LcOVGSnP(I@ z=wtg~17wv&M!zpQF*Wzj<#*hYAe$X8Hv8s*Pi!8v+k1nF4Qn=8ty+hGzhc``9_l$% zAs>mHct+r$(aKtM35TuCEc#d-q=%lPGwoOp z!~}e6wvMCOfI{w9YQBx^J`AC5Aqc5wkgKTUyv`(>5EsSSNGeQTSt_<8!Wf9fZeNRH zZr$5A`!%26{F-{{N%Q?{fJXCrzIkQF&47v?^t_6i@wn&9Df(e_ByWvoNVI5;M9;Cn^&>7%eU4^TzKb{8M)L|=5L#e58AVnn z_KO`5GXC6E;di^GVQrldAWt?{%&+7cCHYO<;GVBaJ$Q(?ObXL;#VngN8T=;yV)8DKz-e5%q zYA)ENs4Q|gkk#Ze9JmR6HvCM^vjRiQFS|%GUHV{5 z&t=nA?GEULdn-C2P({0 zU8fXVzG+E5@P5h?feoS)7z~ZJKxSCB^;H`Gg-&p4kM_cgpFwB4%R9Ms;Ld6*5z2^9 z%(710A~??->uGTo0WXHQPs_FuRi55sh9CJ`@D)hJcns*5YFU~km5haHs-Ai!h1VPI z?O6K&tA25+nN4MAgl8A)pq;4t5r|uIT~dF`m> zQuQ)6jFqUT`Z;wFj`3FSP_M(!$XNoWMeVBQTR)vrE;4?7=Vd0%tW-&9g}4yT=;w^+ z8GZJzx(SnwbaPk=@=jC$o=^`_E=qOVE?^aeSL{U>^GbDkl-f;=e3)YhD0B{g+reR3%EetxC~A|H&~P?+2Wor)dD z?1uVdcQH`8GVDo{hda;3xZwq--&3I9lH)h-?Z#DfopV%IOowygN7m&lWgt13Ot~E! zMUnk3oHjD?=(<%^7S<@_B0x05>*8F4dJBU?*EE`8(><9XDm2!?uD1IYI*%9wj4ggCRV(8pL z(fGy3Tg#DGab4q;bYVnMJL1u*Sxk4MZbkqI4~eRkJ8qE`TC#Q4KdHujInafvT&efYysm%nr?TQG7PNS#BzG!4l9US4 zrsVxZ_gWrCG>UM{LMz&!_`6fl3k`#+0e31LV0kHy|KF*=f@0EOK{d%pKxH&RkM(*p zZ5bwmd|K$pMkF)3{D*+Zg;YFrIVI$`c3W;Ur}p(TW(kr(nkyE{cM;S=UoH!sE;a)V zGa!WBFTku?4h!>(x2K=4G+%fbJ@N@v2_mde@Dqc1L7`Ft_4+ah^8ZmIplD3_H_gYQ9?}z zYu1nj8DcDIE|yx(c2KsR?UYAbTHAY}Bv<4u*w@@eVEyEAqK!s;cfH(|oV@VXT)_)O zpC&6|I)1DbAMlwE)-1PaVpHk=By9|OIq|h7Rc$D+P*`z%JW|&$0rL5U7`IEbFH_k2 zc@7dL#6-d&pNz!Ts4GF>>Fbx?{iI7i@Jv)}5J?P4H5oCWjLnPsoJpZ&^S4> zZQeF`e|zOsVC18ozs{UHxOkRU%;!yv9zCa+nn$9Q7cs{pJsv?fpO(<2@9pAeYL}3d z+c3-{(wEBjaV}K|Vm+nom7PiY67)t^oC~6gGm=J*19B?#14*kPRWQv`7sSXyT-PKT zqnm$(L!%OH#mX-2~t2$Wq+S{ndOR0aZ(be!lt6(Dzqf0U+_&jz*bJ4^uLK&l7cW|#{0U#!@&ovB&e00|nH~xOxxtP;&G-~_N@aq)sHZOmRL09K*cti9G zzNh&4ym3)ceNpAGNXeVjWy3L>1SA3(0v!B=(`K76LSz}{?(tlW4nJU&QK@8+}op_P&;)O{enR03udX_ZZQ2m(x6l_Q^;OS4LlSZ=oP%H@)#19&&ib zzN$rmZmSP|b(dP^{dMwv#0VY+I6l-bJvSva(`x231o7hH7A{SqPULcut@0ca$?bxV z^n;$t%?YO4Fs&WQqVT}?0>YF4pT~YuhjQY`p$eM4*7@_KK7OAt4GKmNpBr@)S>*65 z!DptVgN4Uan9vj8Cyv|Y^J0d+WWeB44t$fyIva4xp|kxJMW$%EGkLqV*=|*@xMHIN znca}o%n(18tCH3B*LN-S0ChL0ag?(ep}KD=#`$m}8kwA|k;p*NCyy=Uir;ObnhAsb z#&~V^I>@S>zY*3jhn+KaKixW8u3p?n_P!Go%=%^gg}5A%8BTVIRGV5_2cHrVcjr=S z7cZZKE=%`Qx+?|j4y*)eO4QIi!W(kPOOM7iAo2+|{RyYMp6$M(Dc@W%FSZ=qi@5)i zD=(2MJx5hCs6_%&Mg}W1(?wul6PK{SdAB_@2xf+MdV}Wwk*ePlbY&#ra`X6OU2o?iQzsW6R1S=?ZaFE$;zg4e&ZO zyZkmTr@|J}*hs@Ky(D!r^>OtMYI5L<>>GmIF61V|fchR6i7|CTL#woN^vf%)VQM2n zg4De-H_U|58UNoMavL0FEgZPkwu2W+*#C6MMwvA4)b1N1Ghrcx?>eel?0o$tO5YSY zdoMz8t#>+q-tBRl8|!?_BWL`Mpz-Rqu;ReQbcS6Qn-GFy_anzADAlnR zG@aE79SvcX42sH@2g<90gY*3wMyZV&KEk72BcUrNP@%!+l82KQ6{%zCggEbfp!|IL z!#ripHnDpLculJAtLAkD=XtZY+#7|<@`TIi^Wig@NGT<8hwJq1tweuM2-5x4S|Z6dj0OY_Y}Q{?M?eHU^)$PA ztnMAu>zk))Iq6bZw02mYv;~?SCTFQctPf((FIBY0^F0WNhPT6tIv?NzdtGo^zHX!+ zr8j?)yzV1QHhQ3Km{zUar;#%4ZIpM2|D;G_VXH8sf1TzQdR4Dlxc5zb%(|$ttUdOz zxF`yj7skHIWf_R|Op5m_JDqZ_2lKh@yrYrC-mBQ;H3gC8L7IK$1>(bN1}CqsyU2tAu?+>zXo7y5^@any?tp z2feV0@1=$i;QUT);E`9?Cyx!r%h1+EWdQ!;qy0ZN+9On=twk6R5X$rr5Y(W#JSJe` zV$<)ei5vMYU)unP0!DO%65R7#kZa zu0`up35bFf`s(SWuH~-f19iWy!Is8LH!r^`71B$OOKy%dX;{o>$%{^xOWvbwmkZu* zpNo{1=?+Ig52F_oF2AD;SBbjI&;Zcvm$&3kc5JQOQ~_EU}Glm*MYnrc3ejJj$ubRv+U4ZFOo zF#G-Xtw311M@npD1MoqeVAnedewSL*f{OZ-M`hK>KC4RB}DczuB%Qk@X(Xa|W?EJW1i4>>7B(SbxeZN<#m5mrB^mB^1 zdlOuKfG4H(h3i;T7ub;x*QO}pxA^AD;um3BdU$*k2>GJY77+X?YrOUA90 zy06~nh{2y}4a+(OF`Awy2{B}NbKN~itu4;lW#dxV&<{gTwlJdgJIyyE7g0VFTA!Xa zSC{FvRA1D?1^^dhdbk3BMm&jr9gr zlBtKEW9(*vW6H-5bMFLmob+|^^$PPT%7IL%L+{o_K4cy1p&`TV1EZj;6Xx#T-PcP+ zRoxybpb)h;ugZ2m%fp?wqmYdjCsiPWlul2z_pA zyOyyeY0^`bgPPs>X{XqNK=1>H7T-Du5_Ha4AKZ<2VT85R0XoUkzB_Lmk8{@-Bv8{p z(O!sq@8fJhk1m4|fK!2TPo<$e3h`;RZ(|`)gF~?em20UX0_X&nubkb?ZyL7x)%qgm zcTTn|LbTK%#^N*yDS0x2>R0znWL(oJMm$8cYZdmZI5N$O+PsMCagzWI;%4F3aO?H; zGz5iT=oXbyu!k`bVxP|}*;g~Qiwt}gwyLKoc{$kSyVZMBRaTqkFDN^7G2z`DRbz=_ zBo{CFN?1k))_~-KRjuSiPS~Z|T&Bf0=Bq`JLaGDeqgHds(@N=MULvkYKFifT#Jvk| z)e{7V;;+bjNUJZl=BDjD_0H4yFMXqZ7Vr*lNXTRfQ=F7j&S8*k6d0ygOtjLVRz?iO z)k!|QdT}3|)Sp*dO2gSP0TnIhxo9D&-fYQynQuN-O_vT4G zrL8sG-(h9#6Ei#;w458xn!e+-C9%%`hG%M%w=ZdDU|@R8+#o9jxX$0QYn;2$?6%fB z&nF?ZBz|^I*{~noyz!E~h-cBNSNw&kD1$RBg9$beDoN445pUB(_Zv-k{_byFtgkt4 zjcqU>v?LbMHpVnRo(g4UnOu4IBY#$EAptINZXdQ-igh8sUeaT~;-;3$0QAiC$X7Z{af7ngK^2zR_y)&ATFUfT>>HJ&gY%&F=|*D+vLt1e?$M1@fgyDk-WL4{e*^eb);~5I5 zg8Z4j?!9fs#~9}w&`l!Mb`lNpV|Iwa$b7QEe5@eMUa~+1`IH`j;k^ZPHdJ`B08NX| zO$XqRdhjXn8@^OspU9x$D0-5ONrPw(nIsVVl??~UstMy$6&ISZ{90X&{+J+Nw)jupSA_e!ydm7~y6|p7^5t2Ia z@Xnvj4D1D1b~t*lZfWpTIj^}QdF&{N>eqj7?XVrWSC=@$wk60P=#%1E&|U!`3_}5X zS+VULkbtZoXUSL>YQRoAn|pcf6%@&{KJQC=o&CDYoO-vK!!H`J*C6n8 zgp8s9=L>~rEp-1IgS7O4(IGu=$L|oR8GO?)Zz0|J5)jMR^@aTh_qTUC6sLX; zq%q3k+W;I{1_#o^N^FZnh6sX6SHt-8#RD$h;grPeD#o8VMYL*nZ>?vSj@4%Xkxupp zB>OAeA99_a-##*FT|%o0kz^R@uR*O_A`K9n2|B&z1&5^YR|2Ey=q5G#V>YmX#~a1j z)3cs8of=9BMa&zeLTI|1=`8F!+iR|Do_qfbGj(PRRx&wd`Pfx9?9vEE+oR&$7MnGE$A=GIrU zRKG6t5ZL^%B_XT(c0twz&DW>Xvo)ttEg7YO^CkrBE zpLY|vO~XF#+wsYreCd#CCBsl4-jR||<1A1(IkHxtarSRs8OK>+-kLzpUa~AK@Do)! z2jyhn{h$bpY_JNVaU&;x4v@H6nzzxn*C0gnL#$^Nlp~1q4u~C-GB^SOLvNOy`Ii7F z)ZN8R6(zqn7^zWb5w^12OC-MU`bFJA9w5g?a(~+_=@zjhAZanU^v$?(J{Al6>6Uj% zl;z(S1mXOI8;-Dw(ZZ4}ur5ke*Zh5OcG{~0G7K+0Li4s`y3oda!W>38yS8k+FBMbT zO5JA&_ZOC}_}yu<;tYE-(EDq!BL9Z1#lwhEjYsOCMZ9iKA~(5w`qL!>S;1foOo6-X z56B{njvyonjsn&0AYrPKh8G~jkr5m-)J`vN`HuryNx@{YhYsTHe&Mc+JJ0SPk};aG z{BOh(1VY8DTf+vjyL;i|W|BM1rvW7=@Eu|CKcf#ja?Tx9AY68afV$BmPP>qA^#Edl zaF&Mc@s@i?6$US))uY;JCI(@|0>Z8m)xeJQG6f{OQ#2~*(%DY}19vsfXXd|Ie?R0N zEn)q5;+>o*!1F+L&hR$;kR%wJX)=Nd2ylb$|KY>hQC1mvnT(EdFwB}z1K+akhg<*+ z_nnAq4A+;Wz(yd+AE?MZ1c)nn$$km6j~4tAFNn8Xs5Q*(I@+K4%VR$iiYF}5A2nk7 zd?=WpW_$f>1Z8VBG>Eg;wcFx*pS@i5FTO;WLRz)P(Cw?ADbMtiKE!Fv)bm)AuGz(I zJ{W~qr}v(!uArR3g&lL_dvYjgPCoUBt{2g$toiM z^f)~eOBsq!Z7ub1R9I^121{R^#dWv7A}%VHUO)P6mI{3F?PjrJR`1=k_Vx}c;cFq) zY^(f%b~s1>rs9shl$u6987##?-*8ZRkx$u1c~R9a_!7 zLEp~SuxCX?Mi0!+7Kd4H!kZRLjg(k0Ceqk*Zazitfjt|*J{H5Y?~^F*ryklg7l>r? zB-Y%6TZtsur+i#ET_7Z2@Cc3NS^oTU%31gV_GA=Rl8peM+$Q1PS1yEl%b zwCY?)s1b|GWq-fyUtwa&SJfYGx?#3YG}xzp6tqtg-UGTnB3;CBv@+0yos=1O6Fx4C zN7d}3H=`P|cLj(71S8SCr*h4c8)WnVd_nE4!PejD7}Z1jT} zp3$zbZjPRQjc2IGdzrXRH}AS8 z;~>c|3c{Zo!dIe*JC4KSWY+8*?Hfe0DN@x9wtz}r9c$<1zpy+JH^NdEV6wHjwjxd4 z_2Wb_&5sQG?TF2Lp0S5nC1;dQdeDc|sb@-cjMT)rna_T_mR7Qqmt1vx?0z@~NsIO1 zpH+B8+Tc?Wdj259hmqNy6u6d-raaP>KkSngQ^MW-20bi&G(?N_GZ=NYuc^>8?ejN; zX-wcdH#jV^;WmXzyrm}?~_Jw}r9f9X$&nfJdA%pAsFfCt^ zIcm-TG<#=63t+izYkvsZ{?Q2a@Iw+wuK>9l3c9a9CZtC%I-_l=+^0u2#{6mT+EgKZ zmHp}k`wB8$YQTLA0hPd!+rR=nn=G9~5HqmXOj~8d8L^Ba94{tO7icds=FKIu0bATx zL4Qw?x_56D)L_yY-uI2Q&7sY3$NAF4-{5Uf(h0#M50N2pWjVgYj3hX|I|0)kSiLO} zDytXi4@)7rDP55`DZ^hANl>*($t7cHf#r#Xr9mX_*A(Vds6_x<-&mrh!N3COCjd@X za|^eS1FN2?beF!XAbkxSvac-o^pif}m#%^;VtCEzPi`IwFZPj)4TOmICYOTI%M97l zgFl+0Kcdk%M_~Eqi_tk5!V}VVE8GHNOe#*;A_ZUg)##BGv*>&Ou4s`Up*!D`(wY$cQ9O{k%6S1NR#*N zLZVvJR;rEpQ+$MDx{UyY8?QngK_u)k!XoJYV&8Nx_|Pw{W4IQIqPA}GVc!#Yx3&~H zEyYRm0kcUFTW*FP*xq7XZ0MAy%W@SCQ6+_Y74dJ{oZkUsfriZw2VO7H1g+r3cjCV3 zh!=aEUl{&*Kb6gvZGXjNcgvkT#tukX)EGET#XFHx?F)MAneArbZOMjYG5T)z!11B~ zjyQoM_amAJK!O!o*at3&mDI+i`eGbm1SpyW4jtKRvgGy8$71tLTFcOU;P6cy^0;{$ zm!42QwkxlhkLbHZ_VI>c@RZ`rMc04j@CK2@)-$o)2MnqN)aa@x+GvK*r zaI2Mm_KzZH!*RQNi*|B1BcWE%Bo!ffJ~jir5h-*>=Wo*=*Wur)b8QV{Z0*aIIS~mh z)YXNrsnt%s!2ecIeCAcyaIZvvgZf?OrH0({Npmc(`Ve012FQc~*c+|kjs_du@M)ZQa_jpnY2 z)oQWmdF!?-ooM++Y-=~xO&?#GJyZY>=>34>j~JH@I%RrY&Ize{Y3?Mfg2Uvrd$qE6 z`HAzb0&Y>hoLo52&@TiCJ9aB;rDBHYzM;e7A&yZcG=rTYn^e7(2G?;9Q{S4=&jQN3TpYdm=#6II--ysQAkILzE9MU^;K zb=7QqCn|QCndJVc{G*F6`&&&6%|(TufL~Jg==vxRR8CidLS7`0Oz@Gi*dKY?3hVg6 z0!@+Vq#zwAw*aetwg_2K;3&9ynZ-_1MSr#Pdtu7-4U>s*By;|EJSgjNzmk6(&Ar$` zNLp~!`VP@?%JS@ZNMVcZg1J?1+Q{1fnATkouKn`*P>t6mm1%KJ^7f^cs&5pO{+-2g znN=uIsZ;b+Yy3^$x{I#h%;fB-A-3Vt5Qs8(Zr&Z1nRQm8XREti>!)J$!-~WoBY+$@VdId9{O)$VB0oWj=Ib5)w z<*5Nc6W`yQWujgVUWqw?`h@zRo>6v&at(qmX=Wf80&Mv}xp*9$#Ry{NH}${~wUPf^ zrE}>aU$tEB8IisKEpz=#9_WAnx%)0h3HS(;<6`cciTv&WI5C%IZn*2+MHKmjKpfM23$l^NFQ=n!~RauRnpJicZzEvX#_d3C|qjHaf zUWibf2q%9fLeu(+S6B7=k-!(SM&e2w+aPl8Y+RXhkAc36K%5YV{2|>FC>ZDve$O2; z)DnEqVsk~Li#dLY_~o+f!5@0L;JC+z>j6?9mrocgmLy{Ml-|G6u!PGYLj(8$XzMgL zmA1<1F~od|^*@hD#I77#Yv7+ZWUcn@` zCv$f5ZA@=wheC*BO_6xwIo#-^MUQ(Bp4boY8f%#Hn7*^weESAEmghF>J8*KYD z9sE}yNDZ4AEE%sAh|NFq z`p+LyqAelpr9iPWX4zlaSgW+khz9;Z~=eToW}u&r1@{d#TR4Y1NgOtwR|P%TKN zyV)@y^iNjz!qs*Q?vRr2&1P9a)L}r5!Jd#CLrc?~d_mwyDM@zLr`%!@$a#0TvLmMK z=(>*6rLQ^tqAWN$KR_q;h40{0osc zJr(-&Nm^hX3|MXMD)47#`jdDc%fs0~^MhM+1Xze-M6Dz;8X0L2sBnBN;(K6aJokaUDuY6#_Hqv;}PDf}= z4qB2LH2sSrI@x=G9JnkEaa;l+C0s9%++MilqLlByYewH%qSLYWk%4&p~8F3 zk^BTgkO`g9*i+95_`M3!r)l|`!!=WQ%&6^v9AiDA$7-#mdK0>Ri~>a>H2m>$PuYHY zdmB$eIux5$J!yLwm$Ue>6SWP-b|`GU&J9Ga92tBPe+{)p zF69zhs{W^|_aa#RSQQr^H_<4tD(ECGbBiso(*v)+)4S8Q#>`_`W51yA(<~`TW<3Qc zU9uBsTz?4+aI{%jj6#s6|L|Uo%AR6DXOTchTonzdDtmg#Sk>(%3(HIUTg}q9*=cb= z7(!$9Oxh7~SeA`IiNSp6c= z@YhU@TZycJ?}r;jLyvhD_xS*5LDQ2KahLQLS$d*`4Ocs!mLsDJAEmAsT^zNf?BDnK z=!30oO7MdM#Ej{PP)^w`0` zl33}S3?iuMZf~4Pv|TJ+U(}Uw;WJtz#ceouSUQ@o9Z^sTnf8I_ELIK8#u;1+lq>qE z29i9kCRVSTJ18vtlixKHbqL$G6sy-pO>wocBxy?LCBg;*@-I#FuRVM&T>E>9>i2N@ ztx}v&dfSyoe_ebf&pF`U%44QrLFc9gR*HGdzy5;QtRhr=BWhooVcVR+Pbk`^DEEQU z1IkDCA<8ThnoiLDs{Ztdv6-D@KMd%>z$F^8=0y;bge$Z`r}dZ}r2d{V?zv=EugH!R zEDp50D3+7v4$3n9}FH!J_*rm1Cn!gMiV@`+c)v z+j08hlJ31_P1hdPow%mAW<0!OyT$?+t{@~^@o%^B@l;jYX(tR}MGyTQb(gOx<5Af@ zK0GxfNrN~CE3`PDyN{f^T^kDgBd`KpxOos zP(}NDpq}EwM=Zh3nhVJQvqvRpBkW)^Km%Im!Ov~{0pa3nd{V-8G}P3;45*3*`!N>KzFBNDY{7axR1 zj#A!oVzj39GRaXneeSv}ZDrV=cWx`$>xzT}B4T`=uyxe@)Z94QoVs-tT$U1iI!P;) z6k^0yv+D>$!IWbW)G*2@%Hk7K8=b~WFFk=M>*<1+A9Ae@QzzaoGe|^bbe-~bjn6~B z!3{*-mY`>JNbeFX5Z0U8LKZHaXUZRH&r@mb+-Z#XitVDSuh(e9Wv))OAhU_tEPkE< zguT}&f`Hi!xo$|rYD^N;@VXCN>?bZOK(tQer?xa2##QQp03W!OdrrpIM)B3nWEmO9|lKWOS08~F)8yQAm&RUTx zEJt(rsMCKdw)y>2v20oF$NA}I`o$}}p9DCRd9%L>FXLocEk+}~X$ zTNUk&DKl~#gybex#?1BTz+(G@x+=i-*o&QZx_LlaSz$aBxo=XJ%zWZAUwO`fqs}@k zN~Z6X*C1E-gcA42$Zej({l(83a{S^o9t{HNjn(EQ`I$zBscxSipjrXO=fPU@gAU=B z`R8a6O_|?fzfbSs4!n;e&m-w`e4+&iexwdGnE6%8{=?&pDkfK-{_dFa<b#cKY0Q!c zHFD2+>oGibvC8R^v!hT?XYN27A(U^OWP3m3G?*O=^m>^WRwV0p=Id_1?2BOz>w*?| z$XC4(RIVbTfB#F&P1k)$#1||-X ze1=qNXzF=|#NeFacn4lshJ&|xZcD-er@#XB_Q@c0>U6 z^nM=5vp)`bC;eyuavphx58#Hzf(QY@1jhb%BY_0<4q$`iAqha`V`u;k;*!Av@YzN1 zPVrw9_){({7)T6?gM|ZW52Fz-JDYZ)f}8J0Fkgi8FJLgJXFw8gig0M#^d15tmIwlZ z?630&2;hu}KpzKj0A2xd`FY@zW8j_bFVHI9A5iQdHGs~$u)hP$J&r&IQw9G5jpBo5 z2gLzdzeb_G!9@uL7v=AZTO$2)afalupl%W@(ER`!fO>ZrY&Q-j9^yejfLG)HXUm}Y z|6p8D+YlPyX%FwP9DI5myp#VA3<6?<`43Qm2^EAej0WIerx5^xUwgRG{tX;r`2&<0 zCIS33yR&5mOZ}TNKtPcGm0Jk+ACMdu%AfN6<-7S8@LKi{uv!WkG&+t3_{$3MFJP9! zAK?EvBjSN{M$iENvDpJZ5$eCdMM{6b86y;c|CkAZZ3$Ta0zqi~0bPzTLH}d80U{m6 zdB?2x=a~E`J>V}dgTE3Y8vcI}9N6A~=N-@r#9&4RdOt@0Zp#Y9H%9hSZf`9+~+<&$0{(WzhUH<@|9gsmp6KK$X)Qs3BaNgy4gO5{yY$tF5|5Xk8 zcUiys`~ls$6M#0riT_BQT!9mF1OFUnm?Q`MrDpM05g7yjfPj94|0D+d6~+D+@N4KF z;IBz8z<&ddAs__*0*OX|LBgQ!Su}9)|LSxh__p1H8Eb^Dh18$Yql3+~!aFZ~(<4D2s# z@)ZegvdT9CK-YUg3*`%AB8^-E%laF%3rA;xg87erIa zI2*+#cIPwl&02bI8g_P?y5HLxFefa=uib>^?2?t^p1#_b$xr_8^W)hX%&1?PP0UAs z#B@?#=a-A<2no};&JZPTdg9(h5(06Wr9r=DZ#Vi709Rh}vz={xPAe~Z^eI8`DI+=yJ!+cx(qj;`$(eSZ1YgV)Pm&|&a6^4~zv=tQ=NA48gZG~2_ zz(Xr47D*QqbCzUuX|<}ZNcluhi5^Da5{CoGlG?LXLoU-U9+maN4rfxyz*zQgW4l{o zk(Ug(-+2H(#Z}gQdPa}w5d^|tSRID?=9*j=z?Xt|C8PaPk=gfi%4$Yo(sklqUl(L+ zU#6N=ph|m}l_+0nN#51Z06po_LaRLWXSn-FL%Ox5*utbXte*WR&Op%*q&OtEHHQCQ z?bw8^V=_%M((r_+Lb8*fzZ?DH`z9wS-EX*&TtH7V@XLg5We6IMp_ky>w-(G4SL6Qg zfCDOvpOkBxooi1xPf&lyy$*)MzIb$Ad7cTUrE8?dY$s@X@LjAw{Y9={JV5csW_wvrd)YpE8x4J0s53@D%=HQ8 zg`W{EK1j=gKNW)|pJT$0$t=T5qWsweXpZdZCu0IdTC=;IZpcZn3%Ds2v)P4Ko8ZTQfulNskbrXBOaFUP3GAUs@4kDi;&{n=NAi{JX19 zL?=T!4m9zqngWoV(sQr%M6On_(eJ4^nF?sn^ ztcN7y=6J4RXuk>N#B&@H$fEWi^ra~5sZaiCicLA@4cSw8R;9tGKl9ygkwnX+ELeo~ zlC-91jhX*=nowrV$V9jVs?7k%-O@^H_g?kAA3i76u?mQh)ILyjIBR-o_tG!mFzMJle`Lr2ZZPK6T6Ty35Ys~JHX1yR`q1VBkvRrVYr?P90?Ll2Gl?A z!;**({Khd83jlGaPXty{ns&C<&S_zs%WXiKg4jDiy&PvW+0~3VRH0#>(e%`CjK1d* zhVr8TP#p^<=Efq`8HcqcuPnolw>&@-)TzPd40A91%L5lhFrb{6F?5y;K}-q)2bvKGt73S;L>vhB6jrFx`op^`gr~GqgR#EU zhaJVp_*O}p<~r;mBKTlxsu_|FVS=&Ho2G|Z(;fWY@=rq|^WyeB@&nsBg&nTUQ0u!=&Q# z8!<^ki{Ha*0|G;eQi(xyVa=rB)a;S}GP3gG6JZ$b9qX#(T+vV`u%GM^R&g1h?2r5a z?W5Qz&Nq944Cd&_4oN=_`Ih@X^Y8-$Ppo9df1*ef?E`Q=B4b+k$iWQ^Nb$v?R$bjv zi=ZZgq#tte`TIlgy=&1adiMrcFJ{yag%ndJ;>S@l23lP^$}Pyv867}rlaVY-X5E+u z#AUTeuiP$~HS$JikOm=f)fS0LA5bcI|DVJE$0omSQKylSz`&>oz`#iVUz;ESxw`Ns z7|Wi5%4?graZ^8()ND!YW;qF*+?QeR4Y{2hDSRcG|uv3fs=#Zw0mJIbxK99*+XA z0-t;RXL_!CACLQ9PiMeaXqj*TnVrsH({7EyEo+N=-PLY}Z0&!RxnK;oFNh`cPRn>rewPgx~vOwv#9Nn6rgvZy~<} zk-|w+#1ls`P`#nTtT*K^?5iKXh?0T2qsBNd@*k<;^(Qa(-}P^~MgIT*-Zw(Pe&_n; zx%CCQ+i8dRR+Et3?I{01$vBzac;RM7EfYwb>Ki}Wwz$7;dPQChFy#72i}}V(_|I_% z;!Apz+vB4?+!=2rBHbe(#<+iM4nqn}IBVR@)Ft6dQc{HS1m#RwReE5B?J@mX|dW_+;v&|SYxR!_V31AiwzSNHwHZ5iIYHcYNaom0>6#ovGBac8SBWrxG&4DZ3 z$i&U=av*T)$-Kgykt?~lipk>?cJ|*Cw{xxkF>)KpX{{bs=)MG5&2P!ezR9{R&&O`S z)h=M_e_C=5!!hlD6>D9-AU@XO%Jl7dd%SRv^WHlWhl4atDyT0{99X#^{jn2&7_N|;lHBg9;)*C3s3Fo7%K)= ztAvM3`?o-HmQ5*zUi*w3b@_NtYjKggRu#jhg#zZNM+3)h0sAEzuY#Vf|8w0pYq{O` zr6subN{re#c2K_gUAQB#RC>sjGVA-~5k24OZePOfC}~je*G=5nmx)KnH(p_W)a|Nn zf9uC#X;M;EM`#sHHu!EH+Q6?na{hBU9*rw}HF)4achDzyyo6z!h&gLFZG1~7$jXvS z+L9$OkKf2ckchI|fge&KxV0A3o8@35^pe72x^@cj4%c~VUd@&1pv)b%c|B&!h zs2SCreJh2rIEsbIq+R~Xe2wTo}q?!k5s4?*9y(0WC6Wdy;g z&XtQTDW!}c{jB2;xN{lhq_ze6N8vUS>Ei2^%nQR6F{{%znKNt4P2(%*skJtAVh&FIfRA z0f!Z7)G|MZHo{biy?U@3p~y-WZ*R7UK|Pc~Wmd*RJEF)+8m{0fhquWPmPxx6imiy6 zK4m*+AI;w6-RCgfwA+iE@q+tNBck#~=NJt=EDoNo{1_jH5Kj;vUk+P+l9)^_T@28P zN~BvyR`~Zn6{U$gE<9nl2YJxKN(6H2$#kf)Ng~q&(&*i)-3Jy@^B$7Y>RbyyE##?j z1?Ch~O=@B?lJ^XlUuA@dYw4u}jHxo!`f4d z{@dgTK3Ep*N#6+o{3o&NrOYn_5U+MjL3CH8Q}7BWq8N@hlt4Sek_-L|cvpycdyC`*U_ z+gf?bI-22r@N-70Dm}5JDB#!o3;fxgj^>ZfANtX3<8sdAj_=Tyw)Dw+J&lbAM0!%? zPb~8H;S^W7jezuycxm3M@>7}Buhiba5WuBIf*tWF>2Mni4 z$JQxxI7)zQTw|faP!n2J=^Y%Ztab)zLi zD*U?xPvpX5Q2bYl4=JFj4y8ZyAX|i{kdyOxcW5<|+)ERio(|TbJ6qKi@M^JyG&uaZ~Q^ON@d?Bd2I0w`M>5NX+DuPc6ggZ%01FC6QUlso#LJp`LH+cLx%)0FZ|0w#^?owOGw|b z0$%N9IR!qCQur+TgtiA33*$Jgo82H76mKi!v-%Z4;^s8aU|0*BAqF_Pz zhEMA7n>WW=7=SE*b0^@ZG{-i5l`s)zFor!lKkf^lzp?-IM8?!;xMM>g3h`2FYmZHs z8-v1J;Yg$!s_9TXOZ(-&Vn2bp=YJb`=G~4*DN~V$p;_vurqhacYhyHoZjjiw;pn6< zp{f6_snO}f2p^@r+vR= zJ-hWJs}Z5!yy7aSxaM!ii{2UB@X^AJpRRu<6b3@!Blq)kSH!xU0nT%yBc9GP$Q0!LJUVR7ojG8i`vL(^k{g2a zuXNzdq1U$MH4J`v;gtk^>G9Vv+x~>_doe4-z05J+9VhjAKCI@XCDO}LBmCMqRs<9e zO$in8OYxssB^gRvK8IJ`ww$O8b4_ykCvcx`S|nvvB!k#drL7c6qYei;(9gEN=zPQd_y z7*({`?hthCKX}PC)Dt@7-&CWzsa**7Z*M|l@70;rWpn=!?Dw%Ab&qP?`A871n!Ib( z^&+18RydWV5EVJczQmxq9!GYJMz)Pcao85UpT0hpyznF%DW287s6GMUIl2kNgZCb5 zyjh&w&pd>m`!BFhX0Z+V7RqHH>X@TIwag@Jw!<%s;&7dmzK8?%JAR&|s}_>IiaOmHUUs0q@0ZGsS=Dw>ld~7dx=eXfxQav zmcQ9PVicK@ua+bnr>jY(VVbpy6xaFDXCyQ5BK*Ip!=e)NX7V|XGBDd|iAo`sxEM4a zLA&>7GUMM;5%NhfSsZ9{c-IN(_%TS}TMf(2j_(ONRby3*!5B}=yq1-Fvg81(sQ^5i zZ+ejl{dD=~+Ve5?0EZ!*PJ;$R*g_sL8@3?yfakRn^nbyTphy-}BPcMiL-^!fTz){`9sL+L zNJw{n?VOVfi3fF*b5??FCFDVbu8TsdUxUQG$#p+eY-v8C$EUTrPV0xfvaI}CiKCy~ zTDaRn_7T}arWF+`%1o$1D|?Sqd0#<#$El*VpXyscS5qQ#JNnEEe{aNb;P$bPnT z;j5y{arMXX*azxbTxmMMXw0pQVO#6{$W$Ps*_KlO;bZlp#yOQRv*$?vZ`DKdXkB`a z-&8N%sDpHTnp~3>KG0@huk{9z)iUrsc+LYO;?gp8!=r{F5K9AVsJK>r# zx~m|+SZ-bSN39@s>|Ar**d&q~X6#n(9-N;{G~G+yJ?&hs9E;BcfcZ40>jssSO+6LA?J6ABU%~3-ovyWIShaT} z|CD8O-S8q9P6v5}r>+?^Vp&;i-P+wbY^DVddOQj~pc+X?QZw_*$?FG?E+v|Bpc`#6 zbDnp+&t2fF0pn2NE`z^=2v}Gmzk^HJ+d%b(Il~L|zJ(Fit}E%jLG}K;#(N<$B2^C0 znePtP{@ahz>%0TqZo9+D`GLZ%dlA7k{nzl3AU&DMh2{EhZqun>u8!8#8*k?T&O}@iJo0Z`ojKB1y{N3}EUvKJd~Qo{#fFyTze0)E=IX`@$PU zI?IdO3boMXP^A+zX6=9`VXKrjWuHdKvNijDC(l~UZ4YGxLuGCEk{aB00}40Be?fGz zd8rNdc2OH-{N6W+sC8!gz-WpbROZyr-I5he`m>D8C>NF;D{Lcqx+Z88Pf|%2)5uBI zddWUh7C78yMA!7YeAJ7hf?OO#?mm$*x$a~(42#j{IT{H=L-0-0)g!O$(n> z{9Vk>Ia3LJ7mGOw>tKkCb9tIF$?O^PReHj8g3Zs3VNwSU6ykWz7$&qVUF0)TX6BIF zXY@%-B{q#?CkhqLUdmu|jc-jOW<%FDH1k%+0HBTAHbFds!JAU@+AM=$H?u;lKXBDY zdUfR$GhfYRF)Wb{Qw90N=jAW7PU3L379=dc4H5YKG-q}?wObLVUAGWPCvHi+7gB81 z^KXLrQNFJWOE{Q}M%efZYY1Uf4f8&^Z~R2M7^b=yTo7O-=L{e+jtu%9>Lk&w7H?(% z0I0@A7@I@mG6nD`GYn)xJ$=DLWJwF&1+J@GBGUEKg~8MOEJ}!XT-eIP_$QrV;&xjP zNPK@c0^vVnPBNX&o}9|iO8wJ4h`X5GU=!upQW(DzlXxxg)kGOcSM4+7Xj%uZ>9@K1 zpz4=H`@di9+!~Oj+U!Ibu%$YXV8RK#0RD8o*z0PrNE~xs_e`L^_Z@UgvxdKqNN*ZU zcq^-2G3TH3CIsyfo8Y{H!{6CCWHbsD!2?c6O8?O=cl0V8f9A}Q*=$&U{fknTJrvS|N@8w3tN$64ig>w|tY=XSg@Y1F#j5b}d5qP2iDYe$(FA~Ial z3#>0u0qL~DRuPr3&vQBfcf@= z%$ezCwR^*?7r)#^PjW_r7RCDC{~s>=4|;{D^xHJ}kI<_oUy^VE`pS5s7++F}hD(oX zH8iatJADZKRlQ5ig`tV6C=D1ASY?Ya*gB>z3tfi~?H&p{yZKR}2Wd{hK{fh6KfA^{ zro4OIk0vLtg@uE_bq18c23KLVkhup!lguso5wF z$ivDbBNE zMYmdGEGE>W72-EQ@#d_#!240)Itm0h9!*QLbXqf_hNn81AMhl0y4tA zHvzQgO`i<#c9W6b6R&brvwMp-Xp-uWyG)9A9ZQPU7f6ZL<22^OOp@#R>!dwUF5G6S zj)h?>s!0&cRU~ge0_SSfe{qCi^G(%{@G~;nHVJbW$95ojQyRe7=l&oe&EyeJO2(Lz zVW0mFk8NVC|BGhKgq|lJnnIKK2dw}F;Q$9wYqBz+XYs%>7?^&-eDxXP7bcQe@gJxD znf{AUeBcF2H)A=BOag&SE7A)gL3WxXyyJWT4U2~AiE`nruP&WdKR{g@29voU6uR<@ zcku=#${hiSV0apEx}X$uE7SCPV*cM<+Ifv#av9uzxeyK*7~Owfbat~&e*Q1p+L=NH za&@2s(3f%l3I7Vbdv*Bfz(R%~3dZaZmKeGcy4Z+_V}b8rZYCp!^x7avk(;O#YvoBn zD4PxPpxR`YrIyuSjZ6MD-Eyk*yhXq1pYX4Xf1=6b2k;3gy!pP>w)}3C_c1e=@NI>J zt8yj0OVeRU{@_lnqg$ch7LyXqAS9h0@ChTXf7sV_d$z-@?-vZ(I3Kmg>{gOY9jY2Q zKitLF=tgx`yRpLLM0#PQ1MSx)9j_nUd3AJ)4+pmYms-PZosM=7Z`igVzW@FLM*?#NVMh-;ghXi}O>y#Ww4Pu6k_}6y7T04v&q{^FV&~3^DDeb(e$YhEYwt|I7;&+uHe^4L~-z5 zB=^wyDze6sxolYiF14R;yX~Z@9L0VCGRngTTaa%xjXHWJhC>Q*;nM2Bo6H7GzRce;MSG5m!KbmY;geW7aT*Ku#Q6$qoHzKl5y*3Oit+%2LQe1Td5 z%WLfP)u}&qtxmZgyAP)QMYgK3d(vZ4%q=onI+aH-iWC*RL znfQ^5T!fmlNLLOc)gN0bI1^!YT(4|ci`a#)`wFs^I!uH~;h0;%^7a(FDDT978!t+f zjGq?MIHz*jlfM$>)(z~+EHP5{T%r3bp{tVE=B=nOIoB43SRX=v7v(r@j@`x{{*bLA zoF3XE|CCycjML}k$PVnK!arxup^`i@=A(pAzmsKHwJX} zlV-(#(y(?fghBPY$S=hq!nQB$Ujrpsv=zC2el^m>%QxB|ii6v?$cVNZmAe>umHh*T za9@?X#xHHZ)sUY(Cwd7Ins)?{_@~gv+A9+ppn0gNIRvnQHhJbRYtVWb?bFGUjz6Cx z#fqvr+I6473HYKfiSblk8mK+-7PrA;U59i?piD2wZ`L59$+C=54s?&XyC4xo^EY%L z5>8kX-LgH`@jx$}zg4P0AYY=Q$Y9>q0o(obHo9U~qO?M72waf?5mPH*Y(IBP!iUB@2cGzDmAt_a9o9toJD1(+^k9T6e@c_l1syjCBBzM&U-jrZ zIL06-q>T7s99bE4x@X__$dh@+jp>PC3eIr$q`&DVZ*xo|YT~v$i`Qotw}I#9#l~Ot z8h+{{%Z{FI2E=OZ^ufY3wvqi#0y#BXB8SR+b)23NP-jQt{6`h8O++^j0lsbSa5eCQ zuwcPsi?O=AnsI8S)NHBJg%oMHjz>6rANqCNEu2|sHoAQ}Cuuv$J-0DEQJsSO`!SWp zv0Ge=;ta%99^)9B?OvyUzx`@YQ!#2@KV=Pg5AJ$YAamN{bN{`llV1Y^rd(lDog_mV4c!M8Jxb@i!CFtn9c_LDQPZL@|xh?sh}Xj2fB3tIkSKUfxCAiv2Gg#kW61r8=F*ecLXG&5q^ z-?U|1N+GWE#V~2HThbupDK3G5f_l~%5&Kh$D*e;~F`H8iyT~S*m|@*TDBq@%zr+vR z%49U$^7<1?%6mmxoE2P3rmHRu4sa74P+AIRhY(`wI-CR#$|jExOnC!%ZhG0now!}z zq`y5GH@oB0mgWo*DsnmW0!}|Xo62QaciaZC#j^3yh#~R!?^K{yy3x@mX#Y1wUDj{* z>CW*M<()HjPz~h6)L|VJJCNs~+8UQKI~3dT)9BY`72(X_J*=_p z34F$MNXFsAZ}>}_QrB|(XCnTn)4355T`d1A(YR#L#)aZkk+C0u_MM$~=b%y&7MH=2 zbn;*TuImSdH@tVpMqB#^eL0Iv8DxF=sR zi2}FX)o|ZMRUVAm*tq2Q&pd>(p^1M-pz){T!i=SVyxs?F5`$r#_@DT}zyuZ1Q*tW9 zB%e|pYfq=pteRqn8IN=5TdRdOlauOfnyIE_YFQ%-PWT71HdZ``ine^{PMwPn=wzpq zhEuCGjNcTYgmdBz+?X?W$gMu5#I#eyez6oEsYi&os6cmwVi`<0%6nd}JUCqc2EPAq zxw+LiFN^;nNiO8%EoM64-5*06H%R%3ZNlmYik^$U&79N<10sB(3T!YOtfP3k2)-DE zxn~o<0~8he`2|CI)nj-Y$gWze)UHP(#=A0!62tMlcI_?Z-;cgG?U#yc?&j=GhcuWY z-=`ej?SS;>fNlP5Uf=ia`d%m#{4WrK{!KdqamubZ8xEVo*e_*(EG7hsJqkj}j)p2x zLKJJ^&VtfK?r4Fm(J~{kKe1oY46T$x2!T1cNhTms1C58A$~u`R;-1X62%WfJT5{LC zDWD0Rc$Av&RyIH`T$$QMbSPhTuldCQEkE5P`9%y|newJZK!|)0d-zLUR_v=O+?rFM ze3u^1&>Ep@)axDy4;-Kqv{@}1h~A*xk4Nr+XhvD{tuRy{oB=cJ7#N zx1Pc0Ez2ha973h4B^a;ysB6uJ*(H<2KK$(K)xTQ&_med2j6}qWUl}jw< z3sV2$bInt6!>(8|9C?N>a4>Vo5cj@XG_O{x8tM#FP|JY}d=R7G&ouK_e@v#5bTxyF zP_ODYBWFML7}aMY^+==+NUK20X}8I_@yV1^Le8=hq)lGh3D&`83qy1%%XB!ES!sd@ zREJ^%F4_+JsHq2s{oZ1a5i!qFfBp092Wz?ezX=#6e+ab_1{@2?nw_ns1CqO31Xc4{ zyBEr^tC#_Ag=Q5ZuNgj&JqE@iEL({~4o8GOZqd6Ft#ux&V^gG0S60q(1wD-5@+0OF z!^ZQgTHrOlo&prMlSRD=zYCLd8ikeapms;to72x4;ygafBg*+E*6I_SpyA@>z!=P| zyRxVFa!Uf-N6WHVn?dzGouC}(%oZfE>S{gv%7&mZGc=1qi#!21?+qTL{v*MKo9{q0{e|7S)Ed_hNn;}lMnV(cxrYQCaC(Yg zBaaoV=AikuxwE0R!TMU}&9ZU^w7)N+gQ3^qWMTgEmwtNo?q@CIgOBr&F0a0$wQiB_ zl&)9VbX{J<+zB{e9O&i;_g0yPKJ@P{S84C|*^V4;p4Rx*alOT|Gq}$TTmugSPaD9x zAKNhefLwF1S9XD;@d)1%V6z|tNQ&OX}A<+lp5Zd^nsq??>2K`v)+8gskHevNHfeo$x3Sy!l$o#4RD)sR=%GTk-yO#e;@hr;58C zf7T18McNr$kKNtrNWLC=h4!1*zisVeEA8iPH@s5Z@E*@a0eUiroj}fs^=Yl4uFiu7 z2p;G9DFL>hndMe2_d5tyc{Sag(l7W!M0(eU3#c9<``xU~zF2hb*9?4Yfz3dqh1#61 z1bgKQgIT6V0M_lKrcJ#{^LJ#I*af24e@5|sH{Gc3EC?MxZ)J+Gdb|a~GJLd|jivW| zj^OyJbi4@1_O<75>mKORyz6&VhR3VBKUMH5&HhT(Y#T^EOV$*MM6m=k`XRL$A~ALu z|G?@AwXr~6p5xsR>bxRTV#Womrvh?vnUavEv2r9+<+of8j*7L3Bijs<~cKN~pu9 zM9&mj@67{7^}g|%w^wx413n+OhhEA#Cd)_B4za!Yf+4dirKDxwvgGf* zQ0sDp{Io#wRo~ap6O00+U$jis2r06}541%uM`<#NOb^;zG8E$3M(F&m%P-A2jl!=- zaCT`ZRpmyyo)Qa3#loSC6C1eGDvPe~&>rId7EE^S@}QpdA&>rih1eqQ&K`o!fV%-S zhi5om-v2Kil?&e?LV*qj_CyN?M)E&AD*1>RC%K0k27nE~JZu%FemnreU)Yp)?k z8zb`KputNbX(S~vWhL38@J@=|pq$Q?M?2Rd*3!Ey@@&KOt|-;iK;xhTc`mQ%)q<9m ztnC9Ys{`wuPyOqiX|Dk*Cnu*Q(ccH@&Q`sLHUHdu?*n}wos|XUCE#C+Q661I@FcSe zIDF?rfxFoQ`Qq{8c9hgRpD^l|$n78|3DP_o^W+j*m|KfHGDXwKayQjTr+wS`P|Q4{ zB3c3QqZth!tzXs=K1lX(`DjI_0^CF8cLJxhX-6Dk#}PJ23COOn>ERV=69>%(S#Y*= z_Z`vIm_{fEVQ|bzxG~}!`ROwM=1G{7kKIt80S{b$atTQT^aIR00rrHMy;M@6>>1eI zTCH78QavOioa*EFx^d#B_pC7F>iaEs#kJIMy^bz>@B&YU&}VNS{E z*GO6vZJ0Oh26~ZGX7-Me?ksh*5!T+!{o$Q&N4FuVP=~=TeP*PrW9%qmHB%LJvQ!eS z!FEHZm-@m8;bR;lJ~v0*T1%s=;eGrg(2Bpzg2`c6bUj~P?AP7gISiWgIfg59^^EeF zU)q1N63hx=(q@G$ikem$G|QSxY2d5oXB0%^tnz2*=~{27%FYq3`EO%c@YY5wfo19` zYegrY2%BlMb5j!@)!b3FP+-}<|LuZThhy`^F`+E!h{?Pam(y;2J9iyow%Hl64pWbV8+LWddYpbTA zQ9pyH@nYw}9d~7==Vm}ZgWn97gSl(qtcP0|?x{t1q(9~K8qOGuUaX%+iTMS-5z_yp zfpIj2=HoObLsx1vw|~y&m$@=zOfImx`)ekOl)|cGAz8|Qob9srto1h;@I%Mh3r|*G z%PVpLo?QTMf0Gv915q$sH5?b*u`(Vh*l!x783_Uj`53o(mI<(UQkrCONmXf1)6HhA z+OD+T{;?Wl<9};%Y$Z(u3&)C`cdqd>FHldpT=yeib;{hVoA^Lo2;oED#KcL^;?9iE zQ=b zcPeKvuYs3ZcwiZ8DKX`K0M`KNxb~P1Pgj?J+l{6(g+Sp3$mN&04 z#29q2SzRdg{-NDmel7k>*qjsQ6vp21;;?A>MdY5@@G+nbd&spV_KHxj(*qolzxe(>OYTS6gUOdbJ5p{~u z-i}_igeIVPocop=&3#PreXGoXjNNT$ zZQ)0YCwtxLt&l=yVZkPa{QAmJp27ym5MpSz45XaY^P>W zS$y~Ht8RSzr)K*EZS$lT^*8${ zq$&AuBtoR<95n*w_%`t3Ja;p{{H4-swid3zNP7^`c^abrIqPLz)n2>$FMZajt9eI` zaIWp$?bzJ_JTX|kP&H3p+5<hDn{7$_3uKMgPqeJ&8#S<|{-so#i$p&g+oy@F^$T2_ zO%7pKRO&}dcHtwNzHnh#DIRr<8o@Y%`I<*~ zer1e(Y_}{JcfOy|2#VO2L_E`duXy19>KN!#c@-2LWq$o6l~1Ug)A+&+nC!#404518 zV?D{E+iX{9dHrQRZi3q(^=FAs(Zhr_UbzF}#`_M}6ZI2%s0*G5&k}!0KenA5D36Kf zd2Xa1vM-LS6{m4yp1pI=t6@3;)8CzPWoo|O^p0?Kjh>HLL@nszZn4C=TjI3rt0?CnS0Mxs9@|ykq1eJ+#~OcuN7{!Zg)7SB zBTJS-WF~y`*x0V`Bc(F^W2zeAD`Sakg0fw6#_MSc66zq*TMoD63vo*@;kO1!i~N7f z;#s@R7ZwTsK|=lBBDZ=A;2|qM!Z2ZfLN?IXjhXz1SDm2Ts6qQd_Sjot2qM`DD$o7m z{CxHwayRd)ejnhKKG zElu5#hH!h14wE8G@QV!CjUN=;pgT5+FXoE58)jhC_* zDruuEq`r(7R#%8jFZnS4`U$!pLS6OU>yACPnA{XM7JIi0Fhqn45;#spR54m53DbkW zqn?}iUP4slYIztTLtD)w5!^H`&hb?+xa-%lK>NdsNz0BS3{VO~xVPcL+`r`L{6=oc zr1%CGnEJ;TJ#)nuz0xg*J8>dntA1{Ij(nb=429OGj^Vl-)_zXGfyqB)WZzOPPWa-w zJS5A&SD_BJ05ITTkx|#17zlQ8lJq?U!+cs;JgzvIQ*}!d8)Z|e`%#n1@w1BL6hyGNm6M#iKoX34Y?8Ma&YYBA<4h+=Mmuj0QoOOVD=uW;GsOjKH?u}{#3xu!! z)bZtB+UY(!8;m=FH$7Mjv5lbZ_WD)UmWDhVYqibxuO#Zhqk*bzHorODN-Ny`-S&tL zDExO-4Aj9KzrhtA)C5WkSDlova?ZLborBqOMmy^^04z920{+B|84}Wi!2>&c^vDae zoW8r=L0RX05qE>*Ly(^yXK4*&hWDTgq`Tf?=g`z3IwGzKyR6VB@W@R$bQ}7DMntCW zUq`cES<2Y`D*PuNc0YN^JeY7>g85F&&M1;QPt-agX4%17GRy~%Xna_;w&BSS=X_gI zu8QAj0U9~$Nk2wGFmh6}hBHw=7&V0g!GMk#aD=kBWbXChw4q3i!9!j(5 z1#2{M3mWkHtvvrfT%A*JWzpKMW7{@6W+&;OW81cEXN4=aZQJNL9ox2TcaoETU!A?H z=Bk?4bB+4=yzih;JygK^-AxaoX^|MW3T5FA3gG8cfyPGw@8T}BYy?Me+d}Fmg z5)}>$Uc)|A#9$U3qj)vAo|OhP$xZiy^*86=iB`~6;^dv-0gChd`m&pF{59H&4dVC0 z^tR^C#E17%mNv#xmVU1=RV*Cvi%CPsOMiq~t~yLixn?;*;^>LKh7Hq(jW8G7y1)RC z0Do(tpX?osNc{OR>pdVLM#U+3Ppbld<8t7i&{7|dWbOzId*Zw7QD~Q!=0{a7WO_(d zEh5c18N9l{Ua?)}4SFoFtMmVvxYkb54zp;BZ5r#}5akGP$jepR5VR8Ix}nHU7uI$= z1{)<-L3lT75xy}ibA|VDrc0i&g*jg*1>97-aQ?-EyFttQ0Hu~FRp9267O(TebNI{( z>cG7@eZEbh>o>)yl6_}^vD)j(ihAD25k7ehFkU0xyY}?zlP&$n-*%AyWlVBLe-IHA zq_Uu;SLftEr8hnqW{bauLbWFvF6va9M89W6M$R9 z!K`N7=lSu#?Z{5_v9KU-{XBanXk|xOE7Y++@%w)Ir(l;G44jEcfYv|p9%VB&gN)coGrC4F!II;r30I+;F&yj zcN!og;L0nIzSV!N3y1;Q&#fah zIUlgnbvfD%ruO=I^~DUh+!_}>D{Cuz3VI8OIuYgRe1UEU$edtu@y37%OwX$1+2SdX zA*)^T)9lt=vxRG5Nf;q0-zy0uxfM#8hBt*c_Y$ZxKjL6w?M_f&QI~zWijg#?+ISn zMJFbu<=V`T0<@3?+vK@e@%g^uj%xv7v*4FEb%Wwo|8{ zc}NcXYW7!|1bca0ERM@*DVM_A5(y>9?bQ;hF}TYqh$an4zu&185C9Ht0>7dBZjlB5 zPep^f;re8<-zC)rD!bL;W<=1Ui4Z^SkivD{6BvhF%-Dg%G3#2`GUCqiMkE`&WoW^V z%%k>uyzxZXWFq$WsqxGkALR*h-h=mo;TMcnZeGnRI*o6j&zpRV=+9oF`?^g(iJk!4 zR^!YM1~us4tfNH$O=wIy4q1o#B zEVTh`&Vjh=td3rIMS*Mj7bBj&;^_hg-+Rm_Y~6_{Z>d0cK(yT!oojnxIaBY zCtp*)!9>`|u!Q?QV!{R$ZpqD4Gxskx`9Vs;K37Mpa8aqDP65+-R}m}z+y!+?YBALe zTslb*1c7V>z>HrZpB$44`2gv4k$|v+nf7G%JImo1;XTyoA1MOL;fF`i0#IR;m@6}0}HPV*(7|~x5fDpXF zzr_E*{}o0Zw;-ZvG{)t8I|Xp(-v0o6e!d~@pvWgm?3jV2I?x5Imr*GO(TnJY{Sw+p zb|4XqpD_rFEroK&U4b1~V;f-II7X;H14v$`>DhYs;x_0G?A4YI@Vjr7`zFe9{%I-6 z7_Kme`Nb90UOE(D~>#qq895V8GS8?_7;XO2x91Z$n53%9C?sAqb4=9kssFM{`$QiSY5 zA5hzxgVWK1P!C;@_{=h1+mg^I0SxBpH05IM?GDcChtp1pce#YX`F_C8=yyV zvK|nYB+W*7go6EkD}BOJrTUTFC0D_4O4N``-LV?2P)VV3Pv3Qhr$F<{Dp+Q(2$33e zBcy4*{vykb{|B`21+K8RLBN{M)iU}wIyA2kDjWyZDr7W%n0h~n`km@-uAv>G4y#8lu^BY(w?dGEyd$(;yhWDN zBCfR{?ZMEQI)^Z4#S=2eK~sb~Cmq6r*|l-A_w>J~&x*tDb(|rrqQj zW_`Z4r!6gr20_{sf<^2@Ba=WX*RhJ2C>!}D)u(WiFVIV@%F70vD?{;FNq2r{bY2(YoRf=#cSvN5YbXPYJx$yz zn#UN>{Ipl%U%x2-jgdB0kIcDT9P7b;Q~MZG$foq7kD#e(* z(5{Kd(6kEkURKO!TI84}>y!N1Mp3(e$IC!Q!o!Dpr3;q79sFV^oKJm~;pOsgHugWX z4zT|YAVx2ibSK~2YP|omj`&XinUX_H+?j>~B&)sZ<1eFrF-mA@R@u@U|1yv;l|d&3 zt=Js0mEB9BYe9oZq8pSG!fL`g4@++N(~z(3)h>HY$2ZsU?3=h0zcC|!@BLNQvc0M+ zofk&+DtJBVo$>QN`+6d?_3Lx`{0qzh=__iNH%d6EW>}$DEGmI^01-DwT`}oWR#W2# zK#JwB!HAq@5q3eHshg3)?wFe>F!P7qpaX$6i_JLT_@d}e%M|l3&o8RhCbRM)A$W0%Y$lsH`d^?>8BXRcGR9Wix2)b>M@h} z?Iuk(HMPFltuLm4f`d&*Qw=t?>~QlWpgF=ig&tCCsR6;epzK%7E)AEfr=N|JmzArf zp_Sj`n2p`H<%1*tFK10bpEsbN@-&8PWt?UnXeD#S&W*^Zlg_47>Z8%1f`f^j-&1&b z_~2VPIi&V9b~NR1U2PJl=)h0o6xP^G(I6_a*o>4@J*Al(0v$D!44T7oHWOEpRtXGN2k>~Hs_8%EJJ0S<(Bia^#^{- zdzTh{4$t@X88)Wpv;ecFEzB~f>m8-^~V&Txla8{b+?NQTKKmok!$l3znnj3Q$9 z?B|h$ZsmWOZ2-k%#5$03s*P0!0M6H=i>@FpZ4e>k&L&`?0tk-K)%-5S8A>oX9>xa)4mO2!XUc$L@;Hj24 zIi-I_moqHa5yT$F9o84dAyP_D1x~zYJwlH#3{28bnj^z#p6E10K9Y6U0%Z41OLPUJ z#D>#JV?REFRxMNj5@=&V@Wl~bb|X}DzWRvKBd~~t9x4OSj*5eHcvRoJ=;fOxU@3DY zX{4pzL01aLKNxp*Ug&J~4%|VHLHZ*^?SWX)W9k=L+);~{^eVAiXT%RE7Co^Y@ z40>(-*d$B)tF0)J8*N~=Y=?8+iHh7aIX*ft+IM8dyS1=5)P;90)lzk4cF0|uK)yFN zMu+34?ao6(d#Ye(N!k`k9 z+e2;A^=y-^F-1u53J8dtyOSRz|$yFnmT9X3r<`ff;6EZ1_`5R{G1B^ zW(^yZs^2_}%^e3AHxp84BfElPdG<%~wHucQO(Gp6MoCK>+D~Jh(Szn&0kbqa`X)@0VhM`Z6J`>#}$Z@H3D_! z*?Y0RaZX9zHjxCx`)6HQm{Yqdh+~yGXOuB4Q@@FYJWz-Wie9B&n1{F`18#eJ_ZKiW z({Ln;^@X(u#!&mg1d4@%Jz+j_iW9rt3?wN6C`qW8aaaB^@s(%Um(Hm+Eu)MT1Fc}D zi-7YSB8)&cKz_}L{wyBSC0UD-FtoZBX6U{+mrE#@ienRSDsuO|WBBQG==s!ZPGMic z1bbLrX><2V0#WzZqk|X9Zkh>A5SNsgJgMxA@JfFj_8KO!3t!WE8W~>|reCg}8RMGA z8C5f5T#9nt3y3ZW2YS&RtP)B|_JBu%QgDV(78vUoKqrI?xhSlzD*~M(E6Y-3FNpZ5 z@0y+Rh5+4tYar#$_M74N;KGp4xJxM81wG)kvAu(W+`7KW$|4BQ?FfZ-a#`VH6S3#} z!_|dGR zUA?!7sJExbF3+kw-;_nZ-`6&Jw&=>6slR(~ zSqQzcpFiSy`KEAdn7W>L*Ib$qo>D9YB2;-F-%sSJ5bBGm5&jwb`v26WsQ>30X5uxm zo$&3u#Rm%l!u+4?NIgRch*r~9)>g&%1Z zmiv5rj}-vz472w=87vBls6rk0C`F~yNZw1?I4Hrk=HD3lqtr<~_>T4_NPe{=44Dz=8ql($ zW#j4fIkh*}Pd4}Ftg1_a0lzYxbN1Fh5$jS?ji@Ha{jY25CAnG#9J_v#_J>s4Yz(fXDQ|P^DJZ#H5gI z*R^=6*Yj8GhGtu_Y$MT!XV@AuBwHsj*3dhYPcgZOX^N~h@^Q(FL!sg3tBi|;?zJ+` zJAtFdCu=GGJqY+L^O~hQWHQY4Q{(^>Loe!@Qo;e&Q|C>P#uOPfqJdOK?#ENKXBK?JQ<{D6 zpC81>g@>Tj-;!n9_91^n7IH%?xh1oxQ*&qEy|nJM^fUulW6<(UuincIp2(8%X z^hH+5t6~yBjS;xcwqhh~KuxKNmRM&cSwIn-@Q_`tqEo=t{@>Jw5@VZF%Bh+$Wm&W6 z1)v?gPFjoY_|>M|nVJ3A9Oub7Uyb&)?vaz~Ak~#*yiz@+GL=cW#6CG>O9TP|q$cKb ze&xy#5|h|bSYBSBE++9lI%e)38)oi)Km^lGabPV##V6Vx0!n^M35+2*RpU29akQ-+ zMlId?KhARnPf(Ndac8(JZnxy8WD$=&S~~KeQ5P8_aj?Z_qr5Q{3J#qBrk3@_8@=Hl zX&%487JrjLS(_*rY09lFwOnNSd_G=$rd^Hfr*3&<91Lif#nsBzpRPul-Xsj?Rj>WX zqiB`^sB9a;w5Ah}DGPX+YW+Yr7q~TT$)2d#rFaD{lBG6R%AcAEfoY7vyz?aLPGc;) zLy>W4I#C{kGkzeyi>GrWcR}e)S(7*v58a_d{=Mg#CT8_djr1ZOhR<5;o34#R20x_K z5+Z;UpWfmBd+jM9VCuVdBJQum0K)7q7=wrd=x0{cq3;gLl-Ow%UZ}nt;P{yq^e*lJ zBqHt?7XJ80n#3XYBo->}WU;f{3hlbVfZ6jtO5{}-s<4bSEAmj@azvnoQ@|1067Sn2 zfIF4H50VWw8xNj^?Y(g88F>F17QzV?sXMWFv?_Mn}>YlzPexjA=+`{4ON*ZhZIP{b~3LS-YbibtI`{)Wq+ChIznf_<-V`z9#H~zAElz z+ZmC1!%Dm85JV?_O4-~L`C-h4F2}91B^k?Y z2yrj6X;znZUe)^FpKOh6_qiXJI`8k()A}GsH#DKrX`=?5kpxi)SB?V1le|Bl+dX?H zh43ORc}-@XYz1yE)PC0Kh8os6TKKUd4#P!NPd=6+glxS5B2Kjtipxrkd29-9mDz;6zFGET$wxje^yCFZfkd!eE8 z*{l+M>+&i++jhS3Rg(g}FW{1G0k3A6b1kQRyFO|<$J58gb4cz$UH7#0Ia{7-_p$SP`itNAXu z-?mbW3bCAk_|!F@NU}v+S(xzrrI+^-eCD;EO7o89Cmtdu!mOUY9b+xW*&pr6x1+1d z=%22LK_hGkS$?FSbhjKBamxyOB@@v=P(;x!uE$(}esT?u{+`(R%o;p1=w~R1QrK-c z#^B3kJ`;>SMmL2I_XD^=R_+CJwj0g#NbI>l?}O3>gkx6{mmq7DvSHRI))-SFp;&?q zBy5CJ60Jc}0ZWvES|pUBSrqR8rVeH*&VeUGIY9jAe6=nKB`Y$~e9ut_eUI`i=GM;l z4I&-joTQVMO)@#@(C@H7Np&Luzdhza_!EP~5!XZsmQ`>c^ZTtx46h3r=8O5?p0-q6 z5dv!=M0+)qJGMH_H<;$17e&_#+^UL=9Q>A8<|8Rf&yw#choVh(h;NBD)bTaQA7yg$i$b|V5KKL)dxg#asm3z*Ut^03CShu^(q%^CV-!oGux z8|lax>^`Ou;fzS}%1E>|IpF=WI1HtOHh4h&i^*qHl>wK5^mK4n33*=tpHmz9ATJjuHfp@97Z{u@^ zPfsY<>WD8DmQ4goddXSgp4ZvUS)?_hxeZqIT?el?tBog}-~>Q+MAK`wG|5j}gBPd6 z0wxL6s8^OGa^n@9hgFo#*H%q>2)i(p8wNmS7P+(l3e)FH;Aou78i&Lc48s%>E>)|E z5ParrT79=%2D0ZECr^KzElv9t;3~=qmm-rS&?%vCSzKYqjkIkbJtxlyaL*3mY49l7 zl-o1l#N7~SfH!1^1?Alq$TG6^UieLVrD`L?vt1Hj`p(tryPB26PA-^6snKTWwd^{f{ z8&%Cpo+tCXOwd+ThSA2=?|=K!WjzRN6{GxXbzs{tK-#L-krBw=EnD#sk>Sa7%Urc% zs#Ex>Es%I8w21ngc9F5UjJ7QX9}bS> z%kOVS1%*-tNA=Dy{Vg0@X4k|+la#$&2YcZTHK&+lpS-4wrwUWiDH*%=0AM=p{r(3K z@gKa|zXV)ae&2+^726~yO;j2?xmh-T7LI6K)cI&AQlg&XmIw7~*&FYwf&SjMFPRQtAXo>4yY zcXD4+gN4DxR49BNqU>bFi2<`_m*Y%WO;P;i)~jVX&#tFt847w;w|FTzliFvR@;(_G zHM}RQn>+6!s>ysDJ)$f`=b~U(klbp?z!n|EfE98|IB{MB6RMSpedPzR_fj$J$gY0%4vt zf)>}`rX7nBbR(p^9+Xi9N4|!uRU7zW} zTVu4QfYQhA`6H^#5kP!?at>hm7_gBj079>@-!ccU7n;aytIJinYo z6sp_YTDQ2i1)zCnG8MS~_UIXW;_usXPeSc+3%`in)8q|J+jqxKnQe>iT>UKo+G-5x zrr_e9uwd6Q#`m{9w#J)DFV;7$&o65Ll;;bCl;AJqMlANpnlhOaXo0#B#IOj(u|w9> z?sQ6wz*q_yuhVYzdo0zIId6q zeAW%-?FBePZ^{vxx{zl`)}{Mcx-GUSA^P{?(R4%?Q3;{il(a|t7@`;SS2f#})L_P! z)JQ#-8wD!ZWhCbVyr#1=Rt4GciCgZ?0k)HT7mmyqTr`3MMb6#x+{6d>&7q~dp+Q%> zv@*X~M&oA%yC;gOKYBlZix-5{yzLZccnAp9PXG+LDM`q$8sytyu>xWeg`|TioXH@` zC^AbMS<`J56u|62xNcA3M;v$Z68&~Pgy0mC7vB_aid0t?ae%ZuMC6!+MdZOvBVtl- zq?$RA9hl+jSk-3`W}|()@+t}#(3CW){ZAhODZQ=>tq?~E%cwF_)m{ETkH9-uxr!5Q zc@2$T%{$)|sW-)lLF0qIeGOU76(Du^R}yv1WevmsJuwWOJke}qG1b8@d0|Uc_+1#y zf+<$$n@68{^R{B(I-NhY?PLjZo7t++ihu0({oba+AZC68<&VA z3|AYjp1o3*6-`{Xe;o@sDVnXcO*}S#QHiS5|JYew-2Se+NJA`J5RJ? z6ljdIt^9eyZ8ha421-Oy63jKUgay_}&RH`q^3jh(KQplG1R7fF^-lC{1E5(jx=xak zajR%U&bb3xXe>>b^?d6+^*g$M&eRV(RG#wWP=*H9To_n#7K}l=5dWy&uZyt4=@61B z`v!iA;WB&NREZnaA8qv)CHs4AffKIF$s(}tl4)ELdqhaM5kJ#VfD4^ez)MPzknstT z#bCAzM|rtSA;f&te+N8*0AV-hH!#^~saw;B{i<9~oXILO(NTB3}`oKDvcd+rd7ci$jbx_Tq^?x^TyPiYLGT#HL zW4OdR8)1O9I{x=uN{-dgAk8Vf2s0luZXvknFMM&cJ|ea(q^0!*1#bV03DuMg z^FP>a-EnvQ^;AV2Y4zL>!cQdETuBLZs<|Xe%`E4q9N*WcE%WPifv?X?WRUAZ(p@xu ztlRM5NQwKym|)e7X%OjvFEXH8fr84(LY5xZ`qI?liN6V9MIt0WcRY*k4m{-c#32Qf_uoMWJ7VVOz` zrt-c^B<;$3#-PKu<$hw!H$gFvVKJdbbt^UZ`q_= zm~K@@Y;);HxNC7rR8B#;6Gu=)FRMyX=^0?QMX9{fIy$3VD+Qi*g)WodHivc7@P{y@ zqD>GqIcHG4m&m^8Fv;SY>Jj!adq?CZ_Hmh;lV^>4WJxFU_e}fBzFJWw6GoJ`^P8!%!&>yls|6@l z^~mWRw3u{;O9JcX-Ap^9ad8{dP;aB7+~cuttD~q%qPahd{0qX+00j{%iHbwrFtS6S zIT2+eWkRWjd1CJWTB5Fh%fjMf+2Fbo+#PxY;dO7v!i)lHR@#ryLO$@)pQPL+iYJ}A zD$d;A$5w4*@_x-H{_J!W(u{{5Vr3kxL{u5Q>1@eNM&?l|HwbvBLjJVZW1H!luM*`!+-u7-| zoxA^Qv@_^6Wfo8J-LeM{{vXnI4WKVTpp|Zh>VN=ZX&(t{X>9l3*m$Ehh!lE`1&GU8Lnv(a zf@!2k*YA-vQg0mfUVqP45_T5~h7_16L_@;scq4W`E6_8P z8xP$R`36^vkrd5rY82maLwZOm`{D+>YTQ3au0bji88*u#chaxXZ^BUk88shj0CwGe zd$@jkcw=G$|8o{pZBqz|L)dL$Pw=nZ5>qr%gKRZfy`}n#qTb7CGqJZtZZl2 z?LXRXKz(RfB&9JK0yQ@dayH&K(eFNQ-=KC7Ceh$lSPj=jH${VbxsL-&`2yea0rJG< z-;kKshl(SrTPzSFM4HfPH2Wra=Wptd2O-g6 z)cZg{n0G2`OgRqzNC;eGTKK~mw*fM3YJv355WS>@MR|`7N>i%GdkF%S6o2~bk^@Vg zW`X2mF(yQD_k>DsP5bTo+xSX3Xp&;Eu~10feXKg_h2Lb)xX74sOPdJ1=Lfvm;qM6u zLfz~~mz?=O^H(y0dKVuNF_e{@u7^l!kL4_~m2+ppHk<4Xp&J_@C$a}7$|Df19p~Ma`TvL8p@xt^zy$>XG5=Taw+FEW;!EbI zjdDGmAFll}>wD_>L15IWt)}V2jt<8_x-g zIJ;}Ihe9z#*VQqN!j&vedcCP1pd5Rwc&oCNk`8%x1o>SF22KISKGN>hx7MGNgvpkQ_ArT9X~X z3=ewW{g1<1)8#zqz&GOb^FNfr|9!4-7bJkTJNkFFqe4t)$6Z6Sn0La4D}uFM(E6<< zNYiS?BwN^U3|`^U3n_-}~}5NT>g`(MkX$uKgfNztFFieWPDkj*=Sok-JU=*E>=K>iK{O zLrF2l7+!YV>oJkRIXz|jvek%c{8i0KqU0I=O6-UV-FkLd?{oLz3T0sitcJ#xVr=-r;gkF_wfu$x z%**z5N*>$0Y`mWfPU+$;>^0HFA?5(y+GOXHtGM_$n11Q7(?K0m$!6|E5{3l*K32M7 z$W6Tz=8ci*Cb-qW;v+ui2 z5}#dan@Ocq1X~vaJt2>c32QRd;Pjd*ddsD(74+cAEUSsi6vm?~qrWb-=yQPguE<{q zgbM3T5~Mav=M$ef=IAxk*~8L8^1cowTa+^wL<~PtHJ>%(Lp1~&&6k23X*p2hrn-lk zu?+XetzLsGabd~J{SBa&5>v@R1xPDg+n-98TxD)=50Za*d@1|&*=hcLJ)8YsCUydvs3 zN18{0Is(n<3{1Mx>{-$K2M9StSQSWU$Do2*QLfUIR9w+Mxr+oxbz>G2pQ;^npRu{cJnBc*!9*KUXvp_E{XTA=5|SW{M6dFv z99=aBD057LGP{K>IXj;iM@ao0>D~9_z}nmBZ?)OU=CqUw;ca@cwbqo|U*;aM_0_bW zu4=rz?zjIeQ)pglB*Fk^SMVde%gI~MtW<%ODqOC7)tg2O4zsTd?s~}y78N{@afKPw z#aAjfRVa4`^G6n&4T|ekjP=h5)fy;hW>{1dHuBM5sm8(VD=a=J|L_QZA_l1hqd?+! z1}o90#pKx8#n$@1>XTWKiI!mr@XM2k&NQ* zNvHh8@%#UEr}%-^@Ad+`z=IMPzA;s`M}PsWwTsh1#+kp?&PnpX#F! z>4fwXGe3X5o|De5#T~XmE5`f^&lREenhml&=z=diZ~-ns$n#F4Tjt=1X7vD{=Gg|b zu~}sNOhQtb!MFfRas*`e15KEIm2~2r?_DzIG>R3iV@_04HYf{3T+j`2XoGwF)E1Z~ zLHK!atV7M}4ik``LHdG1a^@mbL>|u(+~eRba=#S#7LAG2SF2^BD>Y7^+lbuzJI54* zxhF6kOZd8QZ<~LU>gSPmnEPg}Z~jJ;V~1Q_K`>d_T}A*LPz>qD{i+Ibexl{kc$ktj zt35}i!M*j3l7+W4q(sZiHHb8JqxP~eBO=#?Z%x*Of=4%%z-gEpdMX;Tq^MXB2OpS3rR)CLeW@WMu^I&4QZ($dCsr&k8uVg=!h> zdrU*+4-jr2;@y^9JQdGA^(@9(uK7tvjRRusor#}=qaISYj*bT|FQ{k%;|zCBsXizT z(2jZBBJLo2aHKVyA!)aE`}Q*;`xOv z{FWeRbwVg0VTgySecpo8^#(aJnyKr8^z%e4^-8tqbFC7HT8rN6mUH0i6P|J+xwY(h zmj3-uox!Sa8e8&BwVr(cXui`UMdFSZEPa7+F;*e`haYMqa*$Zzs(Jg-4n(+zQCf# z>IjK@IiAQ$=iW|F&raw2?)X2TNCHe@tK%{)!=){P=;LihLJc?2S;+SIm!l+6SQz&L z!eC+fWu)S6B>ozs(gMZKo7DhtgScMvK+QP#k_J7MLm$P&(&%aiQGI#qj*g-<%0-!k z$KVp{C5i!?ohdFJ(`t!?n5KC32)c;!TVKtS z!z)&o5Azu`uf6 zg-qrvPX{WZ7OM?aORerU31m6U{)YEL{GHw&uz$FMTOGS8UReL4R2&loC9%0pbn+D5 zM!>4ABHTT^%mzsNLCgRc_sWUc(lA+$hk0{Y9@XN9kWJRUQUfw+0^^;VRry%Bf0npmD)SM|TSnwJKEo#e z@y1-F2OADMlNmi;RH9|aD*ZuNQ^*39HMLRSRM1&wb6@T#^P3Q$qrd>{9wUv{U01kY z+Fg~352M6W9TxI-pYv$0(Ioeqp3dK+(@ z_4bDZ^T|U!4(L6`pFEdao1EPeULcO~2}FqzdER~f?G38i)Z#>#DGHt$6gx>S(Li!K zLX_%=S;w6rLtzYP6Kv%!yPkz$npJrr51zge>&X-Bb!Rs-5}0uWapVxnC}Ca~zuy8a zQnhmj*|wsPA_H6~tTwnGNI^rJL|T~Ry42GP*{r%zU+B0T$icPFO4~VHAvuCU5{kpP z@4`AHeKtgD!bRs|T&AsVr{TXEaw>x|5(Px!kZ_qf8~&29ni556Zp3S3fpQEv!7$BB z*3;RfXqS9QQ5PasgvdA%0V~{*uZBn{dPeny`IL}07^5=?uvnw}_@A=b zFx7?|Vn5|O-f#vSXL(QC0zTiKz<;AkBZKi}$oagFV zw6|M2&3)EM#|=iS9ZDzow9oQcPOn$+5P=t6sojTsp8M-Hac31MDXn`qVgqE0ym*NQ z{!9#8;ZnUXWJGk^L4a&41LLNFUN!&9J)hpuRL$Zcxq?_g zcbetM6LAuss1h>9s=3K2`6a3)Tr_SrfbjQhG?TWye{eiIIy<`hH}Y8EnXtRn3OyDOj zY`WoeDS59H2tB*5cCQXh(i~(0Mgr5GQDhNY_JL2Fila`n4kQHFDx<4o_}Mi$I5t{H zASSn16bX_r1%v#MRDfS-MGLo0;w-cMX(+3nsB7L>{(WU;@F)DQc_gi&mTjJSrVru| z)a;x;_`=Kqh%sy*PWn^6SKQOSPaBQTwcoKB@i*Q#Kk;{t#fp`+Ut@1%G_H=GJW?7| zX9zP!{;x@hv1%`XKbds@$8a;L&T$**J{5l=OO$I&XA}x27aTz=F4X1qgP2Q@Syv6-y65JMU8S0Eqf@Zn65?j}goE!I{wGyGi@p1f6I&+2Tt<5} zOBM<++N`Te(|y4ynm)f%Q_(uJqBzoXoCdZ7VORAs-_jDW>{4lnoq3od4|P~#XtAUv zYs=0a$l#d}_2eQ#ji~aJ$M^S?AAX?D62o?}0bbr_lbx&E0LO9Ij$W&2ba~0;GybhB zqoQ1lL$!%vv+SsYfo*Hyj}JO@kCkeOi|*&idOA{$h!gY`m@NEzNx2_*D`BjQgsR0L z7k#FVOiUqQ1OzyIO_AKGS(@!~&zFfVtHpkgFzffkG9ub&sY)X&f)wLsXcn>oCub_D^A>z5eskEpJQEZM-iV`3DSVcJIvFu2{^Rh3sj1?4P zPP+hv_F+Mv;;xilA$O^GLKl}dial3a-pEp^^wDyh{}@d;?YrOFm1?eZZ5!4@z0Pf= z86fI`x(4)qQQGa%=%MCAtEO!k(&JzpW{df~h%}e>EJL&Z5p))Kl%oHH_o??Vg8t04 zxEcsuAi}n`0R^`~1$84Lj<-!(f{As!Lo^47ET-5X5bx`ZpKs&zgFLP=7Af)exiQzF zzT66=2MlznFBnw>;hq#x2mLbt(%b%W)03)zaSMEVd-)yV7y(#oLQvSa}A zsaNP!T9zF7y7Mp#NH6q+5bZD7?B3(n#UEu~9&4g*Ko%q-Y*NCPbLXKdz8J13N4 zkzCh4vG6gmkzB$bKP=r7at2CA(Te?)(54UR_a1)8DaP)g{B`z3++)caWatkm|I)eJ z1%1?)z61PsJm_hGhppiI9=a@j56#8?Q&}=DQUZ#V*X`ziqwt;UY(Rwqn-CRLw50Od z6@U5)(kMcR2t{CkDphpbCq%D~)zht)zhHinGIe5vL;qz11h-ACsiDY{NYIQ;UA#=B zbMN1lUSA7WOgzFA)yIcqP3U2}#sM@0Gy7{B-6RvItAKVe=S$N)44O@4gAwHru zqM%tvg574@tM@>(95>x*R)$z)TSFupOEkBM?EH$i1n=&f2x%I8gg25-Mf~^TD_Vi$ zl1akxsJQ4?iFRS`D@_vJ(wc_}P_x)r3BamCj2J?dbNGkomkL7QkWbOZ?5#g2sb6`A zmoTEXx9^C;KWz2^D|sy>WGX59dwE1zFw4vy;6m2&3R#8BbhZchtxScwMjD-aUIK!N z!(cC{40tFru2X6`LY1*FboAZrSNFvB3b`Nxek2CV1w$^@fLABpu-&kej(pi00w5E9 zNrYkW3}LeFPgG7x{=B%T=0iGVemVE-WL~CZ3MWYU*e?tF(b>3Sw(7tOwweTc5s)fU zvu$@b>&0qQnr##p*kjs#@dlOd*+qcxMnodp{lWU5z?xp zVb?qMv%X%$4H2^EU@N;p4q`Gv0UZ+UazBv@u`l?dQh+5de)33wiv2J}?1)vpRf~oz zDcHoD^e(5{i6__}(odu(A#d#E{u~ZG$Mg!DT`bK5HNXE6)!6HB_iTTQYU;irV!rJ!omu8QJjF%1AwUhQF=Li7onOE@SJRv;AOvwncgWc zU<~+)Zue(FtZp)CX%3%UV2E z1H|NG3x?M?bT`Y=Qmj&rmX5FD{Zhr={Q2+->t11XdYzGq`nE5dU% z@YymbW#gCUTz+oN@{$yAX$;t zK}n57o(?_j7bDZU&7z7;5L@L)-5*uwAa$TqE&AF5EBPub*|lj{a2FWL%llVcPJ26k zHpUdb#ZG>FC>2r4U0qbD^(#?YARe4XX*_%383SJFYbQbu4^l>y*#?aO;60YmD1txbDUwmD3gdaj}&b1`1PiW}-EA_j48N+22<>NG2n|UN?O=3g`FCmavvwP;^bExX8wM!U7$vX-&V$i0-tu5t zWEi>1qhw@ipwxr;i6uYrqGmJ4Dym6kmOr2p_~F zopPm+Pz_ms7hExXX*aBf&*2|FjX*|Mzd~`EglmN$c!tdo9z2_Chr1w8IjJ@7>M{G* zVVbti70$rqX}KIsqe!H%!ZewK(K=7N*>$XCj*kaEArU!w7U$#;AeL9$vFWSnKfUS8 z%JUrJMbTwMZ%e(Z!^#2b6l2t~>g@_eSIN4a4L>I+t4p~bc)r}pDDP_;c++-8JW-^i z=0b-hAUk5X`8rWuH74@|Nt2%Q=`(ILRa=&P_5XGC6;N$--P%yxS}0C~LvVMu;_mM5 z?vfUF3+_&_;w~-jUfkWS#i8`i+wcDOyS!N|Yt7nuvgc%G=gc`RqP;Dop@rM z6Q4xpc4TRZg4e4IulTo(RQHt1k% z@t2?FK-W_izOSM1`dXmwtf0(W5Cwm90gRRxyo;t&$%XFN@yI(6TG=ml#t$M_cXPdI z*~@P&9A-Oee6U=9)`Vb<3#1G7zmX8`cqa`YzYyws?+h5I0=mXhgk+*c?5JT!J=q{&NgWjfE(odFdR{0d3Du~+vonC^s3lY z!2{cSANHF*Ke>FYvJ$%H#w6q@Y{?i;nc5}zOk4xvR?+8+ zuG{(ZvR6(+T+AGOrJu815ndzKr{n#+A!JQ}*IO5%@YWf}wXZqJPe=(7)c1Rv>N?8x zTKErs&^G_u2t#L$XGlqj=QG)h)1?jY`c`GcJ}+0jo4Qvt-QRWrGN0V7%VfOua?u3?(Pjo zw&PGOY-V?Z{Bb});q&FG!RkTMuivpD2ucTeN>bwNpLAfI82nS8?*Al0ugU>DXK?!i z#!4>~E)7{>yn1o(|3%x(BfE811X*d_K^}zs5I-LHN4zu$8=pEnPA;$x!3;HlG%sQz z438?qh`C!6cYZA69LEu5`hI}x(b|tRBDN1V07@w7YPD$$Ed@0Nc*fIw@H}C<{MQ4f z)KeY{`;#}3nnF55`B?Ep7)VX#dnJg?wAf2rpo3l&RHCzV+OB(u_F= z*?1&!9HhVChL2MoTZwy;xL{Y`Vz1jDIcw#lT%ps0`dkt#qCq`UPDy`3B*wGyj-ltU0+(V?}k{hdvdsAy?cX9FcCG&n~BlX(!LCT3v0TIc?sE^j1$_0I! zj|$BZ{GS%{L}2G8q4==I#+86UL+cmZ1p#RXI#8>8D0QlP@ZPmjH(heeerR*x|Yjho)@=rhMnR>Fh$Fy_%8w0a0(Qk|B*vjVqC=1oApeax;I zI3TvM+^)~4;Leq9X8*t~*cb(^^R2f>FW)`wR7287b9{{5zG1hcsd}NN8%zvMAM6%o`Y+QnL z_JpxYMVyL+s@u|yUP_T&ggC6pj1hPdiUhF?M*LKD!jZgZ7@H@sMS;rOwDidVfJNOJI6)$Z&xyHpRK5B`#sHnvUk|j51_QB!~N-Vq`Exm;6 zxP*ES_mNWYQ=I6Syu4I0Nor(jwB`O7517yOj9BQKKSE7-I}!y;nTVu3xNIt*ekd_61}`yaGLT^Im^xf{2sTmI=qe-2{sZv;vfid#g;H ze*37lR`cPV;p_Z^;=ov}8D0fb=|N_Nqn6Lk*rU7-CgT0&u^g3b^dYp4{5SHo=17y( z4Ync~#%@Ylkof}Z7xkpz}r(QpRTI&V{GS37X z!LH`L&Lgm$r0zbR63n!k0IAZw8C448kXx%@pl@ggD`|n*?7)~{T6Y%-$YkuC{E_7g z+?+~|d48>Qk=9#NtWl*{o1q*8UPTph(JWkMBIfEuCS?}WeYT`K{Vu#+ojqSK6a5@K z#_jie_>87xn@hb)2twD_?wUTVV%dWmr0$>k95!u;+cGlDj9kO( zABfgeG@G#wC7c>n!TmgS%2EBM5=e9@)=$h5V*mVWC-P%J z1_Yi?v3ir?4dzQ_0AXE;iV(0NKlT^Ct7qNEl1kGYY9cXJ9!LxV$JEWCd z3ly_F)IQ+6!_YowJyE zy!Ag{q%3u|Y6-n)K`pDZ&vY1O<)5VJGwF9e(OKiTWl*iq2I-O8_14^pr!9n@q&u9m zBJ2Sdboh|3X+dxzn`M&ZKUkf48Cr*!@z0!~4fEEhYsh_7R=AmP7go<5RkO?kks>1rEs|${*LQ#lLB8 zecTv0!*$AdAzo37uHAwMQ^KtSSa)Q6)RIfA53D&Xm3sA8A`B~%R+a4{mj`BcB{d-j z?7pch-8K05b!J``OSf_>JAy*2e5Q&YNmTG8Beo3G=3p5|m7Xt-{+t+z9Ngk&g^{peq?iCX3!=K z@iYDSVm>yjTbb)H1=R^d+1EET?GWxkQ03MdpC%}NN{+;WI$JMEy~GtApM?yC*DoJ# zE5(4EErhNeIKP*U**8hjXnbpcs>S5O?|!gKGN%&q9c~ zKcK0A5d=48tVX)lbEW|tZ4pbn^!wDFMzTS}yC{%YG=^jDDKzE~Lf;p4eLcqWXmJDz z{BeaXDTmO_x}Az5tzjJ#a$~%9!iEFI$SS`{N|d0#OL^)2rOKUv%PPAlqga`wntm8A zGMPgDGeJVJq5^bW!f&+KlPX#^xm!aiB=4mJ;*;h6yEFc0R+V4C0MlilfIQ^o8#1;% zqiG~)9Iy;2$gG610+AE=?>VJI&xR~CET||!`9h_NA%gcXkA;z*oX{93)*eI?+3g28 zqXM2U&%b}8ZlW9dnbjQi_LqbuhS`UnmZ{BTv6z+n+QT{8IRs z-fywGr<`ePeX&zy+cg|)4BEOK9@1KK&PH5gU7f04ZB!BEBFmu&q#hjC3KRL_&GcS` zD-lvjd)IorBv{xs!k56?>sLwrV_yPwy!EX>68`@Bb(#7dQ1%=4jj6a!P?u8ZR>890 z?uIwIGd4U!VKMTFD&0g_kKWa;dh(c`)Cc7~Rf_1#nqH{TwPUlhUQpXwFk2;y+Dn*x zjLwy?XMb^nEKS!AU=hhRO=MmyWXtMNof*qOzwL}ak`e8}ogfu&1T1MxEl<`;>C?k_ zni^tiNv0{#1{Z9`VsQ)Vevtkgw3KA#|L5~NtOE(y`+LNe22T2-mT*NHMq{4-f^l4W zS9AIR(ZlHerEmRFkaI7rhwD)8GyR4`E%3P=P?pxz!5sWWFVEU-e>ux?H&4$mbau$7C3m1D+x=4!Cj zu3NMBz%7j1#=OUU|M?>(@pniV%(9JS3K$~fdEUu(uvE{O4tVBQCd+rTs- zf6`Gr_9CHv*Gr*cj9LGXe<7X5ftjKrV9f$g&zaxXy}8vK;o?5UHcBBNru_rI(?~mA~HULMxoG!e5=Lp6 z;PDwg_J7@(KNB8Qr9WwjK!VqpcZR{F&0$Gm63_0wHw4++4uZalqpEgVN;tNX4A^H` zO1AZeBPidmM>OF}wCOoq!F0N;+bkJmAA0ajB}8(}s=0QykV*e9*v}# z%}8t|^>OXRvR6sUbD!cD?V%?mv7UfLWMmJ+gDH>vXTpq;AGP1$r}RY0(m_HA2=pamz5EFDu?O4BE2Z%X8~bAP>{P~CsfTq1Eb40Qo48nU5;7qpsX2Z{ zcy-O-e2P_{wRaf`Z0Jv4-3Y$AAo@~t`0JmBAd2GeR!^!K*AUc-i(8`Hh&hu1G`-|Y zgDS6mA~_U5Z^tMkO6#u_iDANSovNkaTdr_mul4u8};3H9nfjI zr9NbehdzJH!-w}&o5fU?CHCJ@4Ax0-$)|iT)OzLX4Q*4DJOcF&t z!b+(%*+S}_?E+MgC2=q%8GIpgC-V*43?L5v%AMT9U5A=p(Sbkz;%X59lAX84>0NY( zEcDJnnC!S;uQR4ao>i<`N*?=-5^^%+U0FW9n}uD7p|nk-52J`deea69Ni4O6m(Nj_ zy(%1ft%mUF16~jjqm3fhy#VS@7~Nu5ynS7*eZI7T{pafW@^ros0f!vY?v+-ysvI)o zCZTS>e#Ure4Bn+KdVXmT2>B8=)cZY;gK_A)yzll&59lf0yugMTnq|tOM%@-d@M;g? z40nR=0POkuPQcF=$&B5Bpb2j#J4NKYy1*`lEwK9V>}z1gB6-lL z3dH&a1coV$S%neNuR_A9htUzY^2x%;RXD&YI#I_`z`13dqx!!B2RE=Dw1bP&IR`~Qy2|)v054@^g*hpK}L(d zHi=R&il+xnzh(kj_R?d2r)DuEIlWKI*)Ts`_ct(biII_R7Es~JUdpR6RSi9@Vm^E2NSd%L+dk5HP#X7K%sU`QYI>r8flCwQJc37#3^j)XTF=9 z?Pi|CT7mj8OOj$QY4*6qi5qFmzGlM~nt=u_M#Y44;o!??c%Tt-`#WZ!5qhqcj^!h9 z&)Y|>(0DAge07zEp}QZHKQW{V{Ml=NH_%Np%5@oH^dE?{B zG6UthOOWb|kr=#=Hc3oM-(TX$&AH>cq0&*}>zd+*GRZeD+>Kh{yM#y7x?OjR!QX}aoXRtld=sLh zUZT?QE zuM*UKGI+|laG$UKOE5J=4k9R=p6N9$_U1 z{ss4IKHa&3=6y5Sl@gg-1V@UL(k95s@~1uan4nM{*ZVbMXHKs4+7@hE`7|V1us#3P z>dvV?N!othDv;XdZT}Ac@{)bH`-*NuaXy`Kf7Vu7C&FhyHjyeaI#|`h*gD%MRn`&sgZed0YAdXuAf4fGLB;{Rj%!#zvh+`rQX$lYl-=;b>)C{we<%eJc|YA|?6 zH|~rfre(uFWn8RsAlhA5hguFCkfjl9-9gpeDzbztmA5h*Zs9M*`i2cUGIL;KmX`~( zqMc0=)BlkgFYFqQ1Ln%$UooXc;>mwMFDh}qcv~Z=S(I8oZWekS%P~f&<1lc-XN{YeUO&M%sup&pP zwXGXI1rFE_n4p$&*@=U=hRb<44u8H1rv0=uLM8%Lr?tX@7n;IjskM=F{yM9`mYHj5S+B-P!r2wg$wPWC@Y~Qi>J`$`98)O6lELUzjU4Vf9@Vu{|Sn*Gqkz zvIIp)!hNk&3atV;H%r+o%zRPQn?6$cF&_Jtj)+OS%_K$u+BdZtZI(V0UG z%giexqQYSZF~;{ZAPhqsEXClq9kUu8c5AL8F<$R*tz!`odR6A-$@cOg&AEfT4{8AW zomJgJe9b8v1(|Zs@FF_F3?1(5+T2`dU$C{dz*c^`{TJS-vQl-;ix#K3uQFnfyC!0c z3BN-D@d09t$)n*P%x$;#q$AZiutW72HK_{}@m-4b#O)gH=9-rK51dhaGj?@uzGfg> zfZ%P4eHsTqTJ!{LyEetwl~?^3$e!hG7|ZT4dbUIEE}f*g8nG=RjBLAXE`_9A!|I`a zIL-Jj^?nLqUDQ@}=?@>iJ4@RS9qgf;Hs~3tP!4<6zv-Z~&_=B>x}tb0ntlrTd2WB~ z+)nB_lug42Uc_IrbqBJ=@9{`)nL2WWu2Onhrq7^*qSAjOr*k}zVHTDzeiz!xMdSGb z@RB-b*_Cx^2Hp|e*ZM8RKMmBBb}z?2sSkL|?i43f_PORw#aB!Hi0}QWPNZTZd1NbB z$laz)%YiiJQ5V zX6|td!u9;A`=OTCHVMa2(;;9eK1`kCM>QLHf>pud=_!4z#bw5BOpyjJAj7LCnlkb( z?7cugN(JsbUJG>Y^zMgHezG{5pYrM$Pq2JInp$oGq&r;CkK{;fSosB=0v5sheWHZ@ zgaXl82&(iG&uW%Sz2_ku#F;e;mmCi%q;WLjAO+f$*8QK^ov9qbGPJg^+3wp=QdV+YQc$O|D)Yaw^hP`x*Psm zAk&veNJFvVP5;|!sN}&?Pnym^mKIUBn@r~r#&YaMmM#%m;*AT1;WxO*HGGCL8TuLl zpbjEhudKHUjobCB)m^Jgd=WUN+kA6Tqh9CJ*YiN;_-0q$a_cZq6;T{u1sj^UBz?%Bv(BAgNaMdz&#}rXzi%eve zyZZN;;PgHA*`adxc+6enAecu}ZGLwd3{p_GPB-rHz{Pw?vHf!L4(DOGHwk=3%!nhu zZNR_(QXN8;JrC_JaywH5#NGjV&bod0cQuM@x#I`ssIlpgiX0l`bfo_``D+@6pb)9dV7U-l(G z{jDzU#*D3f`G8|!di!wH&!37#wjL_&1>|C1SAH9=N>@eS@}HDmvVZMAwLSfk=YD#m z6H%>E)I0Jsp?Ce1xymVFH7k*eLoDNw3xTD0HZsga!}77D2t6V8Miq@N0u>{dUk-1ya|LKptqcjrB8fa-n9Cz7({4t0Pl_r8W2 z(Pz%^@21uw0hwjfILx!8GRR7xda;(UC&TU4(JOWnjfyX<70(*7le7ZOe?JN2IwTxm?-ubEA4i7U=)d+cbf4l1$4A(k z&a1AmLT7tdU<6ygjCFFF0^OQA+AWghgiaODdPsg15yV}#PJI2&a!%#1ZN*9W^HC z$&oEC=;}87WrFCas0EKCAmpIWNv&#dTxEM1IEMNT%3ocy&y48{)%{dGuoq62$Zm?H z1Q_Z{_S*FDxo=mY=9O@V)wo)PKFvg2t9)!DtP^K|A!aBi>f5GGRwUxf%@M}V&1A_8 zse!7p`i_$o^y^_28FQru7dMaCzUBRj_FKL*PrfTCk1ZO#@Nc~i&mcV9E!-kMm@_Y8 zLhew7^f$5=`Lg?Yx{=f5dyC|yrqvB(^n5jnwLs>m65u{6%5yH5HN7s9y7M`u* zEic_C{JtdRZj$nk*_i4VAIH~=-NV~DHg&{EUQ$=MeSRj`=xaXwHK-Ees49@7UiJ2n zzxx4vT@Aw^+gdq9t?`cbmkLZ-6Gqz;g@Gx zA?uSJ=lFW2*1{4@IiAcgCwbD5!)usOJ>;84sl5wnqh2jDH($nsH(<@P=#d@w@Z>Yc zoPw-VPRQI{c}$V=gC@9)pzjD~aJ>?5eE8v-^Dq_4pEdQqG2E2Uv?iTpeIU)=5;eJt z55Kx~42Fu9mks0{et$r^n6ia<)R7jdGH}4muqqd6%-`Fno@P2YW-2RXe4r4$xQK7G z5Be}U!&WB6LnmoZ6%Zz~LUd(U#9vVM>;yZyOdD2{b*ZhWr9Ul!rNt^9 zW&&$_*v59;IoHH>9APXC>L{>~4!b90L6aHNiL6bC#|C|4q%;!kd$#?=AtpL z$e<+E(Iinh=_LLY1kDwVKah^a`N_L*MSQ0@P5=)TE5{wy@)uV5R8e2oCw|>qQmc>y zr|u79Xve+W3x$WcETlieOoHmPtq(MZjcmCz#}k*W#RWqY8zrao4orQhQ&O(zJFQb< zMo@Ye&FVobVYyoBElW@I6Slv;#At||kTgx@?WxoFPL$G!!**7MtN;rAw0G^-i#~>3 zEso3eq7W&TZWhbY*Lb;VcZsAp@I-kcshVKQ&YML@XcatZ8++DDotvfu%zJ)EAKl27 zjAY8?dba$8p4}t&Q*`sojla*r+Vti@V;aKuQl1BGQ63GvGZQjM(Y=b~k0TzI^-?z= zwJY8{W2~g=dtd2<4Xs&h$2b{)xKZdAQ@Rm*vBOhxkV)PBt7vqu%=I#Gyf&f@^>Q-K zhZ}uwXDJjdAY-g?blX_el*{;8{P~k@Ce5^gs&qCj%7nqd2HT$N9#2OgaUEjdPpp8C zX#83rvb#<7p4!wD0ohLV^s)k;n8LyXp`x2-$-r{gdBv9fA?}QnE7g?ZuK_Xgw?f~x zr39QN5SQ`B&c@g0l9_r8y(U5*eqL{_lJP(FC`-4z`Chw# zw}c|uT`ZBz(1>`cmQA(4!_m_>iAW?wHT4__68^=Ja&)+f7n0)>@= zI!k))ZjGfAy1Ut~cuy6P9kR#%oOO?hol;J)?JLMkGB}^AZxF{w8|r(N#n%&-F97X- zK(O?C>0d?Mh-GH3ql;T7WWRSg!@Ap--vC6vw2r{%%01`kW;2>sSEV-7hCn&0#RM1} zFH*ud8pE#xi@xz0FowHjw{LkZS{WH(b!L2eisY&(((RoccC_= zFf#gQ%|QZ-|LsilMZvp2`LKx(LfS4n>8=BuN9SXI_I&E(w#_o0H;(8evpZq3mg0hJ_Fx5%!OnPhBcPi6 z_YFWuzAfgS+~DWIi_q&Bm9CdVS`q>j)F8yo0|e*x{C^iuvgQ)#gC_Qi#LCdj$6EEl z*fpT~EO&}IPbkHdS6S2!P#&;C<1Fo@gN$iQrWkLTG(8ldD{n};p&X5|XfU>(8(G)Z;! zrVD*AinS|kvzCc+DmK;3kLR-kp0l5Lhhgjv_EmtoRR>TZfFq)Mt4=FGAIeW`;KaEj zLHJ!)dS9lO?4h45e-{IhmUI-3T()ns%~;&5?8B+pdL;Z$Xwa z)rg0kADTG3V`Gj(9pa=9$L49WYB*#C3}+-BC#1sV(%|I1CNC|@qYgMukgnuKeMYi0 zWi8cYacv#z`}eYd!J=S!BvSwC;9K4cfN7KURi=u^y&Ft&{2J|Abno@V>5>;viqC}U zFd1qM=yyC~5p#P>5=!C5K2xLj6{PIUEeyIoKaXgLhBY^pwGe# zK07}U(^FK2QV;|tc4O7JiZ|E%f_Prx(P|@mXv6DNRGx~QAyrkW8GED=qBFyju#|WC zhR)4xwC(dt6aAND9f7kUPTTA0%z3!Eiq=_&(gYHs4_qm>RPA-{aD2uoGN9#--e&I3 zPBWa+2Fj}4J4?x$x?sJ{F!?vch)wu_$5ynDa|J6t2#werPlutciC@(ff152WG_*=X zt^5$}7U3^UDPIO&*XtF^p5NgdFCc<=h40Sy{(yojTErSHs!OfsVe@7e`YN>>_Zv;y ztS)1L_rH6r;li#sk@TsKT0lD5n;`{8P*0hHPM35Yj_;_sgJIsEYyaVO&_1ps(dw4G zRL0lD?g;WU_tWgYyYz<>`u>j6?0lb#t%*Ye>lslfh<|I%x$WvUfa5MEDI}9XI9Y_OVw1#^4 z&7oobEYVHHQo7lEPblfV7Hw3DHK#IOQK-4k5f_&IRWW@%N5*l0`Aklv{re%xllTDR zP)o@suLgEUSSi*sQN{Q!Ckqv67PfnWLymG*Z<}Xhtr|f_$@z#Z*M*Cg%i~zQE<$z{ zAxoC{4zp(j#mB(xlSxoc6TH~@WVAin2Q*s^${6n1C#NR-4sWWdphdV@9kmD+tf?l& z0Op~`@q(1m3}r21znCDqUlo*sjs#8RQHP$eeI$r>?=p%|JE#vAaown?$JBNfcf1wo zx4iP14QQ2}WQpHhYT8hNK1#;p5WYhS(hIpH{knAc@<_4}##ssa=ZKWzK`@$hr1um1 zCuXzrj>L928qW{;t-6RC!oem{y^=hLdIqzrNkyPzxA8Wm;xNQ}kVcR<&L@f#x20vW0Dg(0 zet{an3ZORfTPnAS5Z7qgu8{0{R}4}hAq#C^3;Lf=UqQBSqY-BWmUp<%Gmh_85hI-U zEs*|f@j;mMR}O*AcM6gNdM{z*fdXMmVxm9OV$OXKBK#h^G8f%BbJx#0+6vq&E*D3# zza2Pkzt{?{J!Ov^iUD{i4#iWjQ!T3q&8B)O{IlFWval3fEHgNO{5Y_dwFq&lk##c? z09NuIRZ#BOY>`$x{=x99oM`yr;1#Sj$^r&`!^AyoZgHQ;DTZGlADM`K>r3WP7WOIY zBZ8>bjkO#%a!<({>BI^li(Tn+%!dP(~?pB(AZ|_*X3l7E~c13l8 zbgl8Nb?$I28&OB3m6@Qhs$91?3K#@o*I@j$2a7pQjv;^36Mh~l1GWkSBi z@%fWzSiRhUY5ZBfM7c|t3Ku}d#%Po+#wbM7RB6?TzV|EbE{(t~4W^(#nzQL+*;mQ} zB2eB3@z07e5^K~%H+5eS(IpbhZ}c|ezq86dsp!AK-VlKk9~5DRS`%aMfIWBEG?z%y zUr)F4XJ@H7cN{^|k;|ejok`4PAiMYQ;G30lDMA`Nz|tX<>064rue5wZo4i#5*?TUH~ zP^y4sA+_!+f)8>OB4pw}`rk_8Po6RsV@dm^XY)K|U-)hC zuzmaE_XpYtgO0{bNl96bG*I;2BXe%SUT9>dG<{M0rdZ4pzz{Qsqx^H*8bw%4< zkst)*_r7Cx&YB`6b)&aw>pz3bG=onV=nl(}drb^Rw~#H`0#XC(hv~(tePU~(_gIT5 zWP08kXp68LE~=sHHl8G{YGUAkrfuw853pA_X%edzIV>DlC5K$`qP_V7lXmHvOb-*(j38?#M^yqP<}{|P)Q?08=pVBLKD%XP3E*~o z*U6YWp^KfiaT--+P7;bsj0IPs4Zp}Lx`egno7hZ$fGaCsbFq1DjVHuw|9%{3Hp3ij z3btVaeaRD8_@a;0hCqzx{t&$mp9hks(%Y;Mi0w_|GVy#;y2 zYCrM(VWl4uRi!1e@q$g{C+Ly({yNPnFwO4$$qh-T$Fz-qO@Sln>SM1ejn(a+0q;1r-9lk$ts4CUgA;t{GbKq^6zH=IT zpiP-tVeb84nJ5t^;auwS+#n6nRmF*0;-Brrn_!9r9}MU=kOZ4g&)3~rPzQ9HQk5n&BQ>MFSYN?o=sB+9^S?* zHFfh4C;#~u19p@MhaI@BU*`=0WPJ1fbmA?kf`Mc?u8%%HL%=>Ds%=oZ-fQ93p_Kz4SnJ zC@d%_L1V+Jk^J`sfCfV&V}q5T3BZ1%=&)9SW)|T1QJgm# z@DMp6xPKH6cEpj_6}$zJ@gn?{bt2${na0oo@mClV@BSfX0zg5L{#PU9C=&?+BnJmT zKtB|i*76~u5y+e673c`}ueQN4AX-2;Gll^q-+B)P3W^P!IYt2{8fON;miesdLH_&7 zkb%D*bT~2OtJq-BI4$6$(gs|D3;F^ezP8jUVWM zf2b+`DR-fPY&Y3nX_EE%3-kcJ|7$M)r6l-IUCe*6!v5g=9}0kPP5-Av`M;k+j)Fh` z)x@9V<8|KNX-Kf@J}9j zH{{n*#(Ec}V^xxOXEak5zyew4kw*_>- zt2Dh=+N9I}1I)z%(=MXJ{*x#L7F@)6LzV^J%4Y`~EMUO=lhu?15qal8pyXiPBBH-1 zf0gp_N@sIE1V{!JU%~;ravHn>CD()VddR@(5YYcFNFer1{|aQ^4goR#{oDRCwfjLs zL7_mhoq1n@T)V)O%W{B!ruu&X&yY*Y^9msO9UQZ4{)V6jY&%Q}#$G`O{ND!vWRm+o RFWVll$O Date: Wed, 25 Sep 2019 19:24:17 +0200 Subject: [PATCH 098/181] Fix Gradle publication configuration (#698) Prior to this change, the Gradle publication configuration would be applied to the wrong level of projects (subprojects of subprojects!), resulting in no artifacts being published to the repository. This commit updates the JFrog artifactory Gradle plugin version and fixes the application of the publication configuration to align with what's been done with artifactory and bintray configurations. Signed-off-by: Brian Clozel --- build.gradle | 19 ++++--- gradle/artifactory.gradle | 2 +- gradle/publications.gradle | 105 +++++++++++++++++++------------------ 3 files changed, 65 insertions(+), 61 deletions(-) diff --git a/build.gradle b/build.gradle index 60aef840a..b36d49e1a 100644 --- a/build.gradle +++ b/build.gradle @@ -18,7 +18,7 @@ plugins { id 'com.gradle.build-scan' version '2.4.2' id 'com.github.sherter.google-java-format' version '0.8' apply false - id 'com.jfrog.artifactory' version '4.7.3' apply false + id 'com.jfrog.artifactory' version '4.9.10' apply false id 'com.jfrog.bintray' version '1.8.4' apply false id 'me.champeau.gradle.jmh' version '0.4.8' apply false id 'io.spring.dependency-management' version '1.0.7.RELEASE' apply false @@ -28,7 +28,6 @@ plugins { subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - apply from: "${rootDir}/gradle/publications.gradle" ext['reactor-bom.version'] = 'Dysprosium-RELEASE' ext['logback.version'] = '1.2.3' @@ -132,12 +131,14 @@ subprojects { from javadoc.destinationDir } - publishing { - publications { - maven(MavenPublication) { - from components.java - artifact sourcesJar - artifact javadocJar + plugins.withType(MavenPublishPlugin) { + publishing { + publications { + maven(MavenPublication) { + from components.java + artifact sourcesJar + artifact javadocJar + } } } } @@ -145,6 +146,8 @@ subprojects { } +apply from: "${rootDir}/gradle/publications.gradle" + buildScan { termsOfServiceUrl = 'https://gradle.com/terms-of-service' termsOfServiceAgree = 'yes' diff --git a/gradle/artifactory.gradle b/gradle/artifactory.gradle index 5626fc61e..7f4369242 100644 --- a/gradle/artifactory.gradle +++ b/gradle/artifactory.gradle @@ -31,7 +31,7 @@ if (project.hasProperty('bintrayUser') && project.hasProperty('bintrayKey')) { } defaults { - publications('maven') + publications(publishing.publications.maven) } } } diff --git a/gradle/publications.gradle b/gradle/publications.gradle index c6b118615..d82a370eb 100644 --- a/gradle/publications.gradle +++ b/gradle/publications.gradle @@ -1,61 +1,62 @@ -apply plugin: "maven-publish" apply from: "${rootDir}/gradle/artifactory.gradle" apply from: "${rootDir}/gradle/bintray.gradle" -plugins.withType(MavenPublishPlugin) { - publishing { - publications { - maven(MavenPublication) { - pom { - name = project.name - groupId = 'io.rsocket' - description = project.description - url = 'http://rsocket.io' - licenses { - license { - name = "The Apache Software License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - distribution = "repo" +subprojects { + plugins.withType(MavenPublishPlugin) { + publishing { + publications { + maven(MavenPublication) { + pom { + name = project.name + groupId = 'io.rsocket' + description = project.description + url = 'http://rsocket.io' + licenses { + license { + name = "The Apache Software License, Version 2.0" + url = "https://www.apache.org/licenses/LICENSE-2.0.txt" + distribution = "repo" + } } - } - developers { - developer { - id = 'robertroeser' - name = 'Robert Roeser' - email = 'robert@netifi.com' - } - developer { - id = 'rdegnan' - name = 'Ryland Degnan' - email = 'ryland@netifi.com' - } - developer { - id = 'yschimke' - name = 'Yuri Schimke' - email = 'yuri@schimke.ee' - } - developer { - id = 'OlegDokuka' - name = 'Oleh Dokuka' - email = 'oleh@netifi.com' + developers { + developer { + id = 'robertroeser' + name = 'Robert Roeser' + email = 'robert@netifi.com' + } + developer { + id = 'rdegnan' + name = 'Ryland Degnan' + email = 'ryland@netifi.com' + } + developer { + id = 'yschimke' + name = 'Yuri Schimke' + email = 'yuri@schimke.ee' + } + developer { + id = 'OlegDokuka' + name = 'Oleh Dokuka' + email = 'oleh@netifi.com' + } + developer { + id = 'mostroverkhov' + name = 'Maksym Ostroverkhov' + email = 'm.ostroverkhov@gmail.com' + } } - developer { - id = 'mostroverkhov' - name = 'Maksym Ostroverkhov' - email = 'm.ostroverkhov@gmail.com' - } - } - scm { - connection = 'scm:git:https://github.com/rsocket/rsocket-java.git' - developerConnection = 'scm:git:https://github.com/rsocket/rsocket-java.git' - url = 'https://github.com/rsocket/rsocket-java' - } - versionMapping { - usage('java-api') { - fromResolutionResult() + scm { + connection = 'scm:git:https://github.com/rsocket/rsocket-java.git' + developerConnection = 'scm:git:https://github.com/rsocket/rsocket-java.git' + url = 'https://github.com/rsocket/rsocket-java' } - usage('java-runtime') { - fromResolutionResult() + versionMapping { + usage('java-api') { + fromResolutionResult() + } + usage('java-runtime') { + fromResolutionResult() + } } } } From fedfcecf0c25ae56b759f0d12f420e1a42993b84 Mon Sep 17 00:00:00 2001 From: Brian Clozel Date: Thu, 26 Sep 2019 11:31:07 +0200 Subject: [PATCH 099/181] Fix artifacts groupId (#699) Prior to this change, the default groupId "rsocket-java" would be used for all RSocket artifacts. This commit fixes the group name to "io.rsocket" for all Gradle subprojects. This change is reflected in the published POMs and BOM. Signed-off-by: Brian Clozel --- build.gradle | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle b/build.gradle index b36d49e1a..c354869df 100644 --- a/build.gradle +++ b/build.gradle @@ -43,6 +43,8 @@ subprojects { ext['micrometer.version'] = '1.0.6' ext['assertj.version'] = '3.11.1' + group = "io.rsocket" + googleJavaFormat { toolVersion = '1.6' } From 7ed12845162905ba3e1df6bcb8611c7144bc51fd Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 30 Sep 2019 00:27:54 +0300 Subject: [PATCH 100/181] fixes descriptors bumps version Signed-off-by: Oleh Dokuka --- gradle.properties | 2 +- gradle/publications.gradle | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index a677784c5..eddfb6cb3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-RC4 +version=1.0.0-RC5 diff --git a/gradle/publications.gradle b/gradle/publications.gradle index d82a370eb..b12d9e9c2 100644 --- a/gradle/publications.gradle +++ b/gradle/publications.gradle @@ -8,8 +8,10 @@ subprojects { maven(MavenPublication) { pom { name = project.name + afterEvaluate { + description = project.description + } groupId = 'io.rsocket' - description = project.description url = 'http://rsocket.io' licenses { license { From eb3ba18b2ee327cb83f0052a789968fe600b2488 Mon Sep 17 00:00:00 2001 From: xiazuojie Date: Fri, 11 Oct 2019 16:56:06 +0800 Subject: [PATCH 101/181] fix: do not refreshSockets after closing socket. If servers are intentionally rejecting connections, refreshSockets-after-closing-socket on client side may cause infinite loop of opening and closing (reset on server side) connections. (#702) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 刘禅 --- .../src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java | 1 - 1 file changed, 1 deletion(-) diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java index 376dcdf73..ed7550233 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java @@ -583,7 +583,6 @@ private class WeightedSocket extends AbstractRSocket implements LoadBalancerSock activeSockets.remove(WeightedSocket.this); logger.debug( "Removed {} from factory {} from activeSockets", WeightedSocket.this, factory); - refreshSockets(); }) .subscribe(); From 9237ff0420e784bc7474f88c2bfeae3c7d915734 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Fri, 11 Oct 2019 13:17:16 +0300 Subject: [PATCH 102/181] TupleByteBuf is not writable (#703) Signed-off-by: Maksym Ostroverkhov --- gradle.properties | 2 +- .../src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index eddfb6cb3..9ebe38d18 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-RC5 +version=1.0.0-RC6-SNAPSHOT diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java index eaff4caa0..fbac4b1a0 100644 --- a/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java +++ b/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java @@ -16,6 +16,7 @@ abstract class AbstractTupleByteBuf extends AbstractReferenceCountedByteBuf { static final int DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT = SystemPropertyUtil.getInt("io.netty.allocator.directMemoryCacheAlignment", 0); static final ByteBuffer EMPTY_NIO_BUFFER = Unpooled.EMPTY_BUFFER.nioBuffer(); + static final int NOT_ENOUGH_BYTES_AT_MAX_CAPACITY_CODE = 3; final ByteBufAllocator allocator; final int capacity; @@ -294,7 +295,7 @@ public ByteBuf ensureWritable(int minWritableBytes) { @Override public int ensureWritable(int minWritableBytes, boolean force) { - return 0; + return NOT_ENOUGH_BYTES_AT_MAX_CAPACITY_CODE; } @Override From 33259c2e28ce23595f78c15e36f7c4353a037b95 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Fri, 11 Oct 2019 13:28:40 +0300 Subject: [PATCH 103/181] fix version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 9ebe38d18..b85cba325 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,4 +11,4 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-RC6-SNAPSHOT +version=1.0.0-RC6 From 5a0c78da1b4889ff8a53c07b94b7889dcd7fd683 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Wed, 16 Oct 2019 15:57:26 +0300 Subject: [PATCH 104/181] Tuple3ByteBuf: fix IndexOutOfBoundsException --- rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java index d02b22586..1a0c1ec31 100644 --- a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java +++ b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java @@ -164,7 +164,7 @@ public ByteBuffer[] _nioBuffers(int index, int length) { ByteBuffer[] twoBuffer; ByteBuffer[] threeBuffer; int l = Math.min(twoReadableBytes - index, length); - twoBuffer = two.nioBuffers(index, length); + twoBuffer = two.nioBuffers(index, l); length -= l; if (length != 0) { threeBuffer = three.nioBuffers(threeReadIndex, length); From 31f1730320487353235735c9aa4135046a6d14f4 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Wed, 16 Oct 2019 20:41:26 +0300 Subject: [PATCH 105/181] =?UTF-8?q?RSocketRequester:=20fix=20concurrent=20?= =?UTF-8?q?modification=20of=20senders=20&=20receivers=20=E2=80=A6=20(#706?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * RSocketRequester: fix concurrent modification of senders & receivers map on termination fix non-deterministic errors order on termination Signed-off-by: Maksym Ostroverkhov * improve test Signed-off-by: Maksym Ostroverkhov * address review Signed-off-by: Maksym Ostroverkhov --- .../java/io/rsocket/RSocketRequester.java | 197 +++++++++--------- .../java/io/rsocket/SetupRejectionTest.java | 21 +- 2 files changed, 108 insertions(+), 110 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java index f921365da..cb17ff539 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java @@ -40,6 +40,7 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; import java.util.function.LongConsumer; +import java.util.function.Supplier; import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.reactivestreams.Processor; @@ -56,6 +57,11 @@ class RSocketRequester implements RSocket { private static final AtomicReferenceFieldUpdater TERMINATION_ERROR = AtomicReferenceFieldUpdater.newUpdater( RSocketRequester.class, Throwable.class, "terminationError"); + private static final Exception CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); + + static { + CLOSED_CHANNEL_EXCEPTION.setStackTrace(new StackTraceElement[0]); + } private final DuplexConnection connection; private final PayloadDecoder payloadDecoder; @@ -91,11 +97,11 @@ class RSocketRequester implements RSocket { // DO NOT Change the order here. The Send processor must be subscribed to before receiving this.sendProcessor = new UnboundedProcessor<>(); - connection.onClose().doFinally(signalType -> terminate()).subscribe(null, errorConsumer); connection - .send(sendProcessor) - .doFinally(this::handleSendProcessorCancel) - .subscribe(null, this::handleSendProcessorError); + .onClose() + .doFinally(signalType -> tryTerminateOnConnectionClose()) + .subscribe(null, errorConsumer); + connection.send(sendProcessor).subscribe(null, this::handleSendProcessorError); connection.receive().subscribe(this::handleIncomingFrames, errorConsumer); @@ -103,57 +109,13 @@ class RSocketRequester implements RSocket { KeepAliveSupport keepAliveSupport = new ClientKeepAliveSupport(allocator, keepAliveTickPeriod, keepAliveAckTimeout); this.keepAliveFramesAcceptor = - keepAliveHandler.start(keepAliveSupport, sendProcessor::onNext, this::terminate); + keepAliveHandler.start( + keepAliveSupport, sendProcessor::onNext, this::tryTerminateOnKeepAlive); } else { keepAliveFramesAcceptor = null; } } - private void terminate(KeepAlive keepAlive) { - String message = - String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()); - ConnectionErrorException err = new ConnectionErrorException(message); - setTerminationError(err); - errorConsumer.accept(err); - connection.dispose(); - } - - private void handleSendProcessorError(Throwable t) { - Throwable terminationError = this.terminationError; - Throwable err = terminationError != null ? terminationError : t; - receivers - .values() - .forEach( - subscriber -> { - try { - subscriber.onError(err); - } catch (Throwable e) { - errorConsumer.accept(e); - } - }); - - senders.values().forEach(RateLimitableRequestPublisher::cancel); - } - - private void handleSendProcessorCancel(SignalType t) { - if (SignalType.ON_ERROR == t) { - return; - } - - receivers - .values() - .forEach( - subscriber -> { - try { - subscriber.onError(new Throwable("closed connection")); - } catch (Throwable e) { - errorConsumer.accept(e); - } - }); - - senders.values().forEach(RateLimitableRequestPublisher::cancel); - } - @Override public Mono fireAndForget(Payload payload) { return handleFireAndForget(payload); @@ -263,8 +225,7 @@ public void acceptOnce(@Nonnull Subscription subscription) { if (s == SignalType.CANCEL) { sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); } - - receivers.remove(streamId); + removeStreamReceiver(streamId); }); } @@ -318,7 +279,7 @@ public void accept(long n) { sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); } }) - .doFinally(s -> receivers.remove(streamId)); + .doFinally(s -> removeStreamReceiver(streamId)); } private Flux handleChannel(Flux request) { @@ -419,14 +380,7 @@ protected void hookOnError(Throwable t) { sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); } }) - .doFinally( - s -> { - receivers.remove(streamId); - RateLimitableRequestPublisher sender = senders.remove(streamId); - if (sender != null) { - sender.cancel(); - } - }); + .doFinally(s -> removeStreamReceiverAndSender(streamId)); } private Mono handleMetadataPush(Payload payload) { @@ -472,40 +426,6 @@ private boolean contains(int streamId) { return receivers.containsKey(streamId); } - private void terminate() { - setTerminationError(new ClosedChannelException()); - leaseHandler.dispose(); - try { - receivers.values().forEach(this::cleanUpSubscriber); - senders.values().forEach(this::cleanUpLimitableRequestPublisher); - } finally { - senders.clear(); - receivers.clear(); - sendProcessor.dispose(); - } - } - - private void setTerminationError(Throwable error) { - TERMINATION_ERROR.compareAndSet(this, null, error); - } - - private synchronized void cleanUpLimitableRequestPublisher( - RateLimitableRequestPublisher limitableRequestPublisher) { - try { - limitableRequestPublisher.cancel(); - } catch (Throwable t) { - errorConsumer.accept(t); - } - } - - private synchronized void cleanUpSubscriber(Processor subscriber) { - try { - subscriber.onError(terminationError); - } catch (Throwable t) { - errorConsumer.accept(t); - } - } - private void handleIncomingFrames(ByteBuf frame) { try { int streamId = FrameHeaderFlyweight.streamId(frame); @@ -525,10 +445,7 @@ private void handleIncomingFrames(ByteBuf frame) { private void handleStreamZero(FrameType type, ByteBuf frame) { switch (type) { case ERROR: - RuntimeException error = Exceptions.from(frame); - setTerminationError(error); - errorConsumer.accept(error); - connection.dispose(); + tryTerminateOnZeroError(frame); break; case LEASE: leaseHandler.receive(frame); @@ -614,4 +531,86 @@ private void handleMissingResponseProcessor(int streamId, FrameType type, ByteBu // receiving a frame after a given stream has been cancelled/completed, // so ignore (cancellation is async so there is a race condition) } + + private void tryTerminateOnKeepAlive(KeepAlive keepAlive) { + tryTerminate( + () -> + new ConnectionErrorException( + String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()))); + } + + private void tryTerminateOnConnectionClose() { + tryTerminate(() -> CLOSED_CHANNEL_EXCEPTION); + } + + private void tryTerminateOnZeroError(ByteBuf errorFrame) { + tryTerminate(() -> Exceptions.from(errorFrame)); + } + + private void tryTerminate(Supplier errorSupplier) { + if (terminationError == null) { + Exception e = errorSupplier.get(); + if (TERMINATION_ERROR.compareAndSet(this, null, e)) { + terminate(e); + } + } + } + + private void terminate(Exception e) { + connection.dispose(); + leaseHandler.dispose(); + + synchronized (receivers) { + receivers + .values() + .forEach( + receiver -> { + try { + receiver.onError(e); + } catch (Throwable t) { + errorConsumer.accept(t); + } + }); + } + synchronized (senders) { + senders + .values() + .forEach( + sender -> { + try { + sender.cancel(); + } catch (Throwable t) { + errorConsumer.accept(t); + } + }); + } + senders.clear(); + receivers.clear(); + sendProcessor.dispose(); + errorConsumer.accept(e); + } + + private void removeStreamReceiver(int streamId) { + /*on termination receivers are explicitly cleared to avoid removing from map while iterating over one + of its views*/ + if (terminationError == null) { + receivers.remove(streamId); + } + } + + private void removeStreamReceiverAndSender(int streamId) { + /*on termination senders & receivers are explicitly cleared to avoid removing from map while iterating over one + of its views*/ + if (terminationError == null) { + receivers.remove(streamId); + RateLimitableRequestPublisher sender = senders.remove(streamId); + if (sender != null) { + sender.cancel(); + } + } + } + + private void handleSendProcessorError(Throwable t) { + connection.dispose(); + } } diff --git a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java index 75e9f5a85..cce53a2f2 100644 --- a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java @@ -18,13 +18,11 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; -import org.junit.Ignore; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.core.publisher.UnicastProcessor; import reactor.test.StepVerifier; -@Ignore public class SetupRejectionTest { @Test @@ -64,15 +62,16 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { String errorMsg = "error"; - Mono.delay(Duration.ofMillis(100)) - .doOnTerminate( - () -> - conn.addToReceivedBuffer( - ErrorFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, 0, new RejectedSetupException(errorMsg)))) - .subscribe(); - - StepVerifier.create(rSocket.requestResponse(DefaultPayload.create("test"))) + StepVerifier.create( + rSocket + .requestResponse(DefaultPayload.create("test")) + .doOnRequest( + ignored -> + conn.addToReceivedBuffer( + ErrorFrameFlyweight.encode( + ByteBufAllocator.DEFAULT, + 0, + new RejectedSetupException(errorMsg))))) .expectErrorMatches( err -> err instanceof RejectedSetupException && errorMsg.equals(err.getMessage())) .verify(Duration.ofSeconds(5)); From dd1e44fe07dc307428adf0379ffe922d2f9b3231 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 16 Oct 2019 18:41:53 +0100 Subject: [PATCH 106/181] Remove deprecated method in ClientRSocketFactory (#701) I just discovered the BiConsumer vs the SocketAcceptor variants confuse the compiler with lambda syntax. The deprecation shouldn't be necessary anymore. Signed-off-by: Rossen Stoyanchev --- .../src/main/java/io/rsocket/RSocketFactory.java | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index b6c268464..44f64e550 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -44,7 +44,6 @@ import io.rsocket.util.MultiSubscriberRSocket; import java.time.Duration; import java.util.Objects; -import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; @@ -277,17 +276,7 @@ public ClientTransportAcceptor acceptor(Function acceptor) { } public ClientTransportAcceptor acceptor(Supplier> acceptor) { - return acceptor( - (SocketAcceptor) - (setup, sendingSocket) -> Mono.just(acceptor.get().apply(sendingSocket))); - } - - @Deprecated - public ClientTransportAcceptor acceptor( - BiFunction biAcceptor) { - return acceptor( - (SocketAcceptor) - (setup, sendingSocket) -> Mono.just(biAcceptor.apply(setup, sendingSocket))); + return acceptor((setup, sendingSocket) -> Mono.just(acceptor.get().apply(sendingSocket))); } public ClientTransportAcceptor acceptor(SocketAcceptor acceptor) { From c5ec374df5af3366ec8074f57cb05101d39b51d6 Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Wed, 23 Oct 2019 02:55:16 +0300 Subject: [PATCH 107/181] fix websockets ping/pong control frames handling (#707) * fix websockets ping/pong control frames handling Signed-off-by: Maksym Ostroverkhov * remove odd tests Signed-off-by: Maksym Ostroverkhov --- .../server/BaseWebsocketServerTransport.java | 40 +++++ .../netty/server/WebsocketRouteTransport.java | 5 +- .../server/WebsocketServerTransport.java | 40 +---- .../WebsocketPingPongIntegrationTest.java | 160 ++++++++++++++++++ .../server/WebsocketRouteTransportTest.java | 77 --------- 5 files changed, 206 insertions(+), 116 deletions(-) create mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/BaseWebsocketServerTransport.java create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/BaseWebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/BaseWebsocketServerTransport.java new file mode 100644 index 000000000..26fb44535 --- /dev/null +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/BaseWebsocketServerTransport.java @@ -0,0 +1,40 @@ +package io.rsocket.transport.netty.server; + +import static io.netty.channel.ChannelHandler.*; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import io.netty.util.ReferenceCountUtil; +import io.rsocket.Closeable; +import io.rsocket.transport.ServerTransport; +import java.util.function.Function; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.netty.http.server.HttpServer; + +abstract class BaseWebsocketServerTransport implements ServerTransport { + private static final Logger logger = LoggerFactory.getLogger(BaseWebsocketServerTransport.class); + private static final ChannelHandler pongHandler = new PongHandler(); + + static Function serverConfigurer = + server -> + server.tcpConfiguration( + tcpServer -> + tcpServer.doOnConnection(connection -> connection.addHandlerLast(pongHandler))); + + @Sharable + private static class PongHandler extends ChannelInboundHandlerAdapter { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof PongWebSocketFrame) { + logger.debug("received WebSocket Pong Frame"); + ReferenceCountUtil.safeRelease(msg); + ctx.read(); + } else { + ctx.fireChannelRead(msg); + } + } + } +} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java index 9b78ece60..30aa0fa96 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java @@ -46,7 +46,7 @@ * An implementation of {@link ServerTransport} that connects via Websocket and listens on specified * routes. */ -public final class WebsocketRouteTransport implements ServerTransport { +public final class WebsocketRouteTransport extends BaseWebsocketServerTransport { private final UriPathTemplate template; @@ -63,8 +63,7 @@ public final class WebsocketRouteTransport implements ServerTransport */ public WebsocketRouteTransport( HttpServer server, Consumer routesBuilder, String path) { - - this.server = Objects.requireNonNull(server, "server must not be null"); + this.server = serverConfigurer.apply(Objects.requireNonNull(server, "server must not be null")); this.routesBuilder = Objects.requireNonNull(routesBuilder, "routesBuilder must not be null"); this.template = new UriPathTemplate(Objects.requireNonNull(path, "path must not be null")); } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java index 205f419a2..948d6f573 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java @@ -19,12 +19,6 @@ import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.netty.channel.ChannelHandlerContext; -import io.netty.channel.ChannelInboundHandlerAdapter; -import io.netty.handler.codec.http.websocketx.CloseWebSocketFrame; -import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; -import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.transport.ClientTransport; @@ -46,8 +40,8 @@ * An implementation of {@link ServerTransport} that connects to a {@link ClientTransport} via a * Websocket. */ -public final class WebsocketServerTransport - implements ServerTransport, TransportHeaderAware { +public final class WebsocketServerTransport extends BaseWebsocketServerTransport + implements TransportHeaderAware { private static final Logger logger = LoggerFactory.getLogger(WebsocketServerTransport.class); private final HttpServer server; @@ -55,7 +49,7 @@ public final class WebsocketServerTransport private Supplier> transportHeaders = Collections::emptyMap; private WebsocketServerTransport(HttpServer server) { - this.server = server; + this.server = serverConfigurer.apply(Objects.requireNonNull(server, "server must not be null")); } /** @@ -107,33 +101,7 @@ public static WebsocketServerTransport create(InetSocketAddress address) { public static WebsocketServerTransport create(final HttpServer server) { Objects.requireNonNull(server, "server must not be null"); - return new WebsocketServerTransport( - server.tcpConfiguration( - tcpServer -> - tcpServer.doOnConnection( - connection -> - connection.addHandlerLast( - new ChannelInboundHandlerAdapter() { - @Override - public void channelRead(ChannelHandlerContext ctx, Object msg) - throws Exception { - if (msg instanceof PongWebSocketFrame) { - logger.debug("received WebSocket Pong Frame"); - } else if (msg instanceof PingWebSocketFrame) { - logger.debug( - "received WebSocket Ping Frame - sending Pong Frame"); - PongWebSocketFrame pongWebSocketFrame = - new PongWebSocketFrame(Unpooled.EMPTY_BUFFER); - ctx.writeAndFlush(pongWebSocketFrame); - } else if (msg instanceof CloseWebSocketFrame) { - logger.warn( - "received WebSocket Close Frame - connection is closing"); - ctx.close(); - } else { - ctx.fireChannelRead(msg); - } - } - })))); + return new WebsocketServerTransport(server); } @Override diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java new file mode 100644 index 000000000..eac091dd8 --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java @@ -0,0 +1,160 @@ +package io.rsocket.transport.netty; + +import io.netty.buffer.Unpooled; +import io.netty.channel.Channel; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.handler.codec.http.websocketx.PingWebSocketFrame; +import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; +import io.netty.handler.codec.http.websocketx.WebSocketFrame; +import io.netty.util.ReferenceCountUtil; +import io.rsocket.*; +import io.rsocket.transport.ServerTransport; +import io.rsocket.transport.netty.client.WebsocketClientTransport; +import io.rsocket.transport.netty.server.WebsocketRouteTransport; +import io.rsocket.transport.netty.server.WebsocketServerTransport; +import io.rsocket.util.DefaultPayload; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.netty.http.client.HttpClient; +import reactor.netty.http.server.HttpServer; +import reactor.test.StepVerifier; + +public class WebsocketPingPongIntegrationTest { + private static final String host = "localhost"; + private static final int port = 8088; + + private Closeable server; + + @AfterEach + void tearDown() { + server.dispose(); + } + + @ParameterizedTest + @MethodSource("provideServerTransport") + void webSocketPingPong(ServerTransport serverTransport) { + server = + RSocketFactory.receive() + .acceptor((setup, sendingSocket) -> Mono.just(new EchoRSocket())) + .transport(serverTransport) + .start() + .block(); + + String expectedData = "data"; + String expectedPing = "ping"; + + PingSender pingSender = new PingSender(); + + HttpClient httpClient = + HttpClient.create() + .tcpConfiguration( + tcpClient -> + tcpClient + .doOnConnected(b -> b.addHandlerLast(pingSender)) + .host(host) + .port(port)); + + RSocket rSocket = + RSocketFactory.connect() + .transport(WebsocketClientTransport.create(httpClient, "/")) + .start() + .block(); + + rSocket + .requestResponse(DefaultPayload.create(expectedData)) + .delaySubscription(pingSender.sendPing(expectedPing)) + .as(StepVerifier::create) + .expectNextMatches(p -> expectedData.equals(p.getDataUtf8())) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + pingSender + .receivePong() + .as(StepVerifier::create) + .expectNextMatches(expectedPing::equals) + .expectComplete() + .verify(Duration.ofSeconds(5)); + + rSocket + .requestResponse(DefaultPayload.create(expectedData)) + .delaySubscription(pingSender.sendPong()) + .as(StepVerifier::create) + .expectNextMatches(p -> expectedData.equals(p.getDataUtf8())) + .expectComplete() + .verify(Duration.ofSeconds(5)); + } + + private static Stream provideServerTransport() { + return Stream.of( + Arguments.of(WebsocketServerTransport.create(host, port)), + Arguments.of( + new WebsocketRouteTransport( + HttpServer.create().host(host).port(port), routes -> {}, "/"))); + } + + private static class EchoRSocket extends AbstractRSocket { + @Override + public Mono requestResponse(Payload payload) { + return Mono.just(payload); + } + } + + private static class PingSender extends ChannelInboundHandlerAdapter { + private final MonoProcessor channel = MonoProcessor.create(); + private final MonoProcessor pong = MonoProcessor.create(); + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { + if (msg instanceof PongWebSocketFrame) { + pong.onNext(((PongWebSocketFrame) msg).content().toString(StandardCharsets.UTF_8)); + ReferenceCountUtil.safeRelease(msg); + ctx.read(); + } else { + super.channelRead(ctx, msg); + } + } + + @Override + public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { + Channel ch = ctx.channel(); + if (!channel.isTerminated() && ch.isWritable()) { + channel.onNext(ctx.channel()); + } + super.channelWritabilityChanged(ctx); + } + + @Override + public void handlerAdded(ChannelHandlerContext ctx) throws Exception { + Channel ch = ctx.channel(); + if (ch.isWritable()) { + channel.onNext(ch); + } + super.handlerAdded(ctx); + } + + public Mono sendPing(String data) { + return send( + new PingWebSocketFrame(Unpooled.wrappedBuffer(data.getBytes(StandardCharsets.UTF_8)))); + } + + public Mono sendPong() { + return send(new PongWebSocketFrame()); + } + + public Mono receivePong() { + return pong; + } + + private Mono send(WebSocketFrame webSocketFrame) { + return channel.doOnNext(ch -> ch.writeAndFlush(webSocketFrame)).then(); + } + } +} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java index 26f598c2d..e94bef13c 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketRouteTransportTest.java @@ -16,93 +16,16 @@ package io.rsocket.transport.netty.server; -import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; import static org.assertj.core.api.Assertions.assertThatNullPointerException; -import java.util.function.BiFunction; -import java.util.function.Consumer; -import java.util.function.Predicate; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; import reactor.core.publisher.Mono; import reactor.netty.http.server.HttpServer; -import reactor.netty.http.server.HttpServerRoutes; import reactor.test.StepVerifier; final class WebsocketRouteTransportTest { - @Test - public void testThatSetupWithUnSpecifiedFrameSizeShouldSetMaxFrameSize() { - ArgumentCaptor captor = ArgumentCaptor.forClass(Consumer.class); - HttpServer httpServer = Mockito.spy(HttpServer.create()); - HttpServerRoutes routes = Mockito.mock(HttpServerRoutes.class); - Mockito.doAnswer(a -> httpServer).when(httpServer).route(captor.capture()); - Mockito.doAnswer(a -> Mono.empty()).when(httpServer).bind(); - - WebsocketRouteTransport serverTransport = - new WebsocketRouteTransport(httpServer, (r) -> {}, ""); - - serverTransport.start(c -> Mono.empty(), 0).subscribe(); - - captor.getValue().accept(routes); - - Mockito.verify(routes) - .ws( - Mockito.any(Predicate.class), - Mockito.any(BiFunction.class), - Mockito.nullable(String.class), - Mockito.eq(FRAME_LENGTH_MASK)); - } - - @Test - public void testThatSetupWithSpecifiedFrameSizeButLowerThanWsDefaultShouldSetToWsDefault() { - ArgumentCaptor captor = ArgumentCaptor.forClass(Consumer.class); - HttpServer httpServer = Mockito.spy(HttpServer.create()); - HttpServerRoutes routes = Mockito.mock(HttpServerRoutes.class); - Mockito.doAnswer(a -> httpServer).when(httpServer).route(captor.capture()); - Mockito.doAnswer(a -> Mono.empty()).when(httpServer).bind(); - - WebsocketRouteTransport serverTransport = - new WebsocketRouteTransport(httpServer, (r) -> {}, ""); - - serverTransport.start(c -> Mono.empty(), 1000).subscribe(); - - captor.getValue().accept(routes); - - Mockito.verify(routes) - .ws( - Mockito.any(Predicate.class), - Mockito.any(BiFunction.class), - Mockito.nullable(String.class), - Mockito.eq(FRAME_LENGTH_MASK)); - } - - @Test - public void - testThatSetupWithSpecifiedFrameSizeButHigherThanWsDefaultShouldSetToSpecifiedFrameSize() { - ArgumentCaptor captor = ArgumentCaptor.forClass(Consumer.class); - HttpServer httpServer = Mockito.spy(HttpServer.create()); - HttpServerRoutes routes = Mockito.mock(HttpServerRoutes.class); - Mockito.doAnswer(a -> httpServer).when(httpServer).route(captor.capture()); - Mockito.doAnswer(a -> Mono.empty()).when(httpServer).bind(); - - WebsocketRouteTransport serverTransport = - new WebsocketRouteTransport(httpServer, (r) -> {}, ""); - - serverTransport.start(c -> Mono.empty(), 65536 + 1000).subscribe(); - - captor.getValue().accept(routes); - - Mockito.verify(routes) - .ws( - Mockito.any(Predicate.class), - Mockito.any(BiFunction.class), - Mockito.nullable(String.class), - Mockito.eq(FRAME_LENGTH_MASK)); - } - @DisplayName("creates server") @Test void constructor() { From c5684ebd2769ed5ba15de8dc00dea7f61fc5d30d Mon Sep 17 00:00:00 2001 From: Maksym Ostroverkhov Date: Wed, 30 Oct 2019 02:41:52 +0200 Subject: [PATCH 108/181] Revert "Performance Optimization Session (#682)" (#709) This reverts commit 3fe5750a Signed-off-by: Maksym Ostroverkhov --- .../internal/UnicastMonoProcessor.java | 309 +++----- .../internal/UnicastMonoProcessorTest.java | 661 ------------------ 2 files changed, 92 insertions(+), 878 deletions(-) delete mode 100644 rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java index d5958028a..35d4906ec 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java @@ -1,300 +1,175 @@ package io.rsocket.internal; import java.util.Objects; -import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.stream.Stream; import org.reactivestreams.Processor; -import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; -import reactor.core.Exceptions; import reactor.core.Scannable; import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Operators; import reactor.util.annotation.Nullable; import reactor.util.context.Context; +import reactor.util.function.Tuple2; public class UnicastMonoProcessor extends Mono implements Processor, CoreSubscriber, Disposable, Subscription, Scannable { - /** - * Create a {@link UnicastMonoProcessor} that will eagerly request 1 on {@link - * #onSubscribe(Subscription)}, cache and emit the eventual result for 1 or N subscribers. - * - * @param type of the expected value - * @return A {@link UnicastMonoProcessor}. - */ - public static UnicastMonoProcessor create() { - return new UnicastMonoProcessor<>(); - } - - volatile CoreSubscriber actual; - - @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater ACTUAL = - AtomicReferenceFieldUpdater.newUpdater( - UnicastMonoProcessor.class, CoreSubscriber.class, "actual"); - - volatile int once; - @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater ONCE = AtomicIntegerFieldUpdater.newUpdater(UnicastMonoProcessor.class, "once"); - Throwable error; - volatile boolean terminated; - O value; + private final MonoProcessor processor; - volatile Subscription subscription; - static final AtomicReferenceFieldUpdater UPSTREAM = - AtomicReferenceFieldUpdater.newUpdater( - UnicastMonoProcessor.class, Subscription.class, "subscription"); + @SuppressWarnings("unused") + private volatile int once; - @Override - public final void cancel() { - if (isTerminated()) { - return; - } + private UnicastMonoProcessor() { + this.processor = MonoProcessor.create(); + } - final Subscription s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription()); - if (s == Operators.cancelledSubscription()) { - return; - } + public static UnicastMonoProcessor create() { + return new UnicastMonoProcessor<>(); + } - if (s != null) { - s.cancel(); - } + @Override + public Stream actuals() { + return processor.actuals(); } @Override - @SuppressWarnings("unchecked") - public void dispose() { - final Subscription s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription()); - if (s == Operators.cancelledSubscription()) { - return; - } + public boolean isScanAvailable() { + return processor.isScanAvailable(); + } - final CancellationException e = new CancellationException("Disposed"); - error = e; - value = null; - terminated = true; - if (s != null) { - s.cancel(); - } + @Override + public String name() { + return processor.name(); + } - final CoreSubscriber a = this.actual; - ACTUAL.lazySet(this, null); - if (a != null) { - a.onError(e); - } + @Override + public String stepName() { + return processor.stepName(); } - /** - * Return the produced {@link Throwable} error if any or null - * - * @return the produced {@link Throwable} error if any or null - */ - @Nullable - public final Throwable getError() { - return isTerminated() ? error : null; + @Override + public Stream steps() { + return processor.steps(); } - /** - * Indicates whether this {@code UnicastMonoProcessor} has been interrupted via cancellation. - * - * @return {@code true} if this {@code UnicastMonoProcessor} is cancelled, {@code false} - * otherwise. - */ - public boolean isCancelled() { - return isDisposed() && !isTerminated(); + @Override + public Stream parents() { + return processor.parents(); } - /** - * Indicates whether this {@code UnicastMonoProcessor} has been completed with an error. - * - * @return {@code true} if this {@code UnicastMonoProcessor} was completed with an error, {@code - * false} otherwise. - */ - public final boolean isError() { - return getError() != null; + @Override + @Nullable + public T scan(Attr key) { + return processor.scan(key); } - /** - * Indicates whether this {@code UnicastMonoProcessor} has been terminated by the source producer - * with a success or an error. - * - * @return {@code true} if this {@code UnicastMonoProcessor} is successful, {@code false} - * otherwise. - */ - public final boolean isTerminated() { - return terminated; + @Override + public T scanOrDefault(Attr key, T defaultValue) { + return processor.scanOrDefault(key, defaultValue); } @Override - public boolean isDisposed() { - return subscription == Operators.cancelledSubscription(); + public Stream> tags() { + return processor.tags(); } @Override - public final void onComplete() { - onNext(null); + public void onSubscribe(Subscription s) { + processor.onSubscribe(s); } @Override - @SuppressWarnings("unchecked") - public final void onError(Throwable cause) { - Objects.requireNonNull(cause, "onError cannot be null"); - - if (UPSTREAM.getAndSet(this, Operators.cancelledSubscription()) - == Operators.cancelledSubscription()) { - Operators.onErrorDropped(cause, currentContext()); - return; - } + public void onNext(O o) { + processor.onNext(o); + } - error = cause; - value = null; - terminated = true; + @Override + public void onError(Throwable t) { + processor.onError(t); + } - final CoreSubscriber a = actual; - ACTUAL.lazySet(this, null); - if (a != null) { - a.onError(cause); - } + @Nullable + public Throwable getError() { + return processor.getError(); } - @Override - @SuppressWarnings("unchecked") - public final void onNext(@Nullable O value) { - final Subscription s; - if ((s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription())) - == Operators.cancelledSubscription()) { - if (value != null) { - Operators.onNextDropped(value, currentContext()); - } - return; - } + public boolean isCancelled() { + return processor.isCancelled(); + } - this.value = value; - terminated = true; + public boolean isError() { + return processor.isError(); + } - final CoreSubscriber a = actual; - ACTUAL.lazySet(this, null); - if (value == null) { - if (a != null) { - a.onComplete(); - } - } else { - if (s != null) { - s.cancel(); - } - - if (a != null) { - a.onNext(value); - a.onComplete(); - } - } + public boolean isSuccess() { + return processor.isSuccess(); } - @Override - public final void onSubscribe(Subscription subscription) { - if (Operators.setOnce(UPSTREAM, this, subscription)) { - subscription.request(Long.MAX_VALUE); - } + public boolean isTerminated() { + return processor.isTerminated(); } - /** - * Returns the value that completed this {@link UnicastMonoProcessor}. Returns {@code null} if the - * {@link UnicastMonoProcessor} has not been completed. If the {@link UnicastMonoProcessor} is - * completed with an error a RuntimeException that wraps the error is thrown. - * - * @return the value that completed the {@link UnicastMonoProcessor}, or {@code null} if it has - * not been completed - * @throws RuntimeException if the {@link UnicastMonoProcessor} was completed with an error - */ @Nullable public O peek() { - if (!isTerminated()) { - return null; - } + return processor.peek(); + } - if (value != null) { - return value; - } + public long downstreamCount() { + return processor.downstreamCount(); + } - if (error != null) { - RuntimeException re = Exceptions.propagate(error); - re = Exceptions.addSuppressed(re, new Exception("Mono#peek terminated with an error")); - throw re; - } + public boolean hasDownstreams() { + return processor.hasDownstreams(); + } - return null; + @Override + public void onComplete() { + processor.onComplete(); } @Override - public final void request(long n) { - Operators.validate(n); + public void request(long n) { + processor.request(n); } @Override - public Context currentContext() { - final CoreSubscriber a = this.actual; - return a != null ? a.currentContext() : Context.empty(); + public void cancel() { + processor.cancel(); } @Override - @Nullable - public Object scanUnsafe(Attr key) { - // touch guard - boolean c = isCancelled(); + public void dispose() { + processor.dispose(); + } - if (key == Attr.TERMINATED) { - return isTerminated(); - } - if (key == Attr.PARENT) { - return subscription; - } - if (key == Attr.ERROR) { - return error; - } - if (key == Attr.PREFETCH) { - return Integer.MAX_VALUE; - } - if (key == Attr.CANCELLED) { - return c; - } - return null; + @Override + public Context currentContext() { + return processor.currentContext(); + } + + @Override + public boolean isDisposed() { + return processor.isDisposed(); } - /** - * Return true if any {@link Subscriber} is actively subscribed - * - * @return true if any {@link Subscriber} is actively subscribed - */ - public final boolean hasDownstream() { - return actual != null; + @Override + public Object scanUnsafe(Attr key) { + return processor.scanUnsafe(key); } @Override public void subscribe(CoreSubscriber actual) { Objects.requireNonNull(actual, "subscribe"); if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { - actual.onSubscribe(this); - ACTUAL.lazySet(this, actual); - if (isTerminated()) { - Throwable ex = error; - if (ex != null) { - actual.onError(ex); - } else { - O v = value; - if (v != null) { - actual.onNext(v); - } - actual.onComplete(); - } - ACTUAL.lazySet(this, null); - } + processor.subscribe(actual); } else { Operators.error( actual, diff --git a/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java b/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java deleted file mode 100644 index 20ed4b469..000000000 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java +++ /dev/null @@ -1,661 +0,0 @@ -package io.rsocket.internal; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.Assumptions.assumeThat; - -import java.lang.ref.WeakReference; -import java.time.Duration; -import java.util.Date; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import org.assertj.core.api.Assertions; -import org.junit.Test; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.Scannable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; -import reactor.core.publisher.Operators; -import reactor.test.StepVerifier; -import reactor.test.publisher.TestPublisher; -import reactor.util.function.Tuple2; - -public class UnicastMonoProcessorTest { - - @Test - public void noRetentionOnTermination() throws InterruptedException { - Date date = new Date(); - CompletableFuture future = new CompletableFuture<>(); - - WeakReference refDate = new WeakReference<>(date); - WeakReference> refFuture = new WeakReference<>(future); - - Mono source = Mono.fromFuture(future); - Mono data = - source.map(Date::toString).log().subscribeWith(UnicastMonoProcessor.create()).log(); - - future.complete(date); - assertThat(data.block()).isEqualTo(date.toString()); - - date = null; - future = null; - source = null; - System.gc(); - - int cycles; - for (cycles = 10; cycles > 0; cycles--) { - if (refDate.get() == null && refFuture.get() == null) break; - Thread.sleep(100); - } - - assumeThat(refFuture.get()).isNull(); - assertThat(refDate.get()).isNull(); - assertThat(cycles).isNotZero().isPositive(); - } - - @Test - public void noRetentionOnTerminationError() throws InterruptedException { - CompletableFuture future = new CompletableFuture<>(); - - WeakReference> refFuture = new WeakReference<>(future); - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); - - Mono source = Mono.fromFuture(future); - Mono data = source.map(Date::toString).subscribeWith(processor); - - future.completeExceptionally(new IllegalStateException()); - - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(data::block); - - future = null; - source = null; - System.gc(); - - int cycles; - for (cycles = 10; cycles > 0; cycles--) { - if (refFuture.get() == null) break; - Thread.sleep(100); - } - - assumeThat(refFuture.get()).isNull(); - assertThat(cycles).isNotZero().isPositive(); - } - - @Test - public void noRetentionOnTerminationCancel() throws InterruptedException { - CompletableFuture future = new CompletableFuture<>(); - - WeakReference> refFuture = new WeakReference<>(future); - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); - - Mono source = Mono.fromFuture(future); - Mono data = - source.map(Date::toString).transformDeferred((s) -> s.subscribeWith(processor)); - - future = null; - source = null; - - data.subscribe().dispose(); - processor.dispose(); - - data = null; - - System.gc(); - - int cycles; - for (cycles = 10; cycles > 0; cycles--) { - if (refFuture.get() == null) break; - Thread.sleep(100); - } - - assumeThat(refFuture.get()).isNull(); - assertThat(cycles).isNotZero().isPositive(); - } - - @Test(expected = IllegalStateException.class) - public void MonoProcessorResultNotAvailable() { - MonoProcessor mp = MonoProcessor.create(); - mp.block(Duration.ofMillis(1)); - } - - @Test - public void MonoProcessorRejectedDoOnSuccessOrError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - mp.doOnSuccessOrError((s, f) -> ref.set(f)).subscribe(); - mp.onError(new Exception("test")); - - assertThat(ref.get()).hasMessage("test"); - assertThat(mp.isError()).isTrue(); - } - - @Test - public void MonoProcessorRejectedDoOnTerminate() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicInteger invoked = new AtomicInteger(); - - mp.doOnTerminate(invoked::incrementAndGet).subscribe(); - mp.onError(new Exception("test")); - - assertThat(invoked.get()).isEqualTo(1); - assertThat(mp.isError()).isTrue(); - } - - @Test - public void MonoProcessorRejectedSubscribeCallback() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - mp.subscribe(v -> {}, ref::set); - mp.onError(new Exception("test")); - - assertThat(ref.get()).hasMessage("test"); - assertThat(mp.isError()).isTrue(); - } - - @Test - public void MonoProcessorSuccessDoOnSuccessOrError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - mp.doOnSuccessOrError((s, f) -> ref.set(s)).subscribe(); - mp.onNext("test"); - - assertThat(ref.get()).isEqualToIgnoringCase("test"); - assertThat(mp.isTerminated()).isTrue(); - assertThat(mp.isError()).isFalse(); - } - - @Test - public void MonoProcessorSuccessDoOnTerminate() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicInteger invoked = new AtomicInteger(); - - mp.doOnTerminate(invoked::incrementAndGet).subscribe(); - mp.onNext("test"); - - assertThat(invoked.get()).isEqualTo(1); - assertThat(mp.isTerminated()).isTrue(); - assertThat(mp.isError()).isFalse(); - } - - @Test - public void MonoProcessorSuccessSubscribeCallback() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - mp.subscribe(ref::set); - mp.onNext("test"); - - assertThat(ref.get()).isEqualToIgnoringCase("test"); - assertThat(mp.isTerminated()).isTrue(); - assertThat(mp.isError()).isFalse(); - } - - @Test - public void MonoProcessorRejectedDoOnError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - mp.doOnError(ref::set).subscribe(); - mp.onError(new Exception("test")); - - assertThat(ref.get()).hasMessage("test"); - assertThat(mp.isError()).isTrue(); - } - - @Test(expected = NullPointerException.class) - public void MonoProcessorRejectedSubscribeCallbackNull() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.subscribe((Subscriber) null); - } - - @Test - public void MonoProcessorSuccessDoOnSuccess() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - mp.doOnSuccess(ref::set).subscribe(); - mp.onNext("test"); - - assertThat(ref.get()).isEqualToIgnoringCase("test"); - assertThat(mp.isTerminated()).isTrue(); - assertThat(mp.isError()).isFalse(); - } - - @Test - public void MonoProcessorSuccessChainTogether() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - mp.subscribe(mp2); - - mp.onNext("test"); - - assertThat(mp2.peek()).isEqualToIgnoringCase("test"); - assertThat(mp.isTerminated()).isTrue(); - assertThat(mp.isError()).isFalse(); - } - - @Test - public void MonoProcessorRejectedChainTogether() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - mp.subscribe(mp2); - - mp.onError(new Exception("test")); - - assertThat(mp2.getError()).hasMessage("test"); - assertThat(mp.isError()).isTrue(); - } - - @Test - public void MonoProcessorDoubleFulfill() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - StepVerifier.create(mp) - .then( - () -> { - mp.onNext("test1"); - mp.onNext("test2"); - }) - .expectNext("test1") - .expectComplete() - .verifyThenAssertThat() - .hasDroppedExactly("test2"); - } - - @Test - public void MonoProcessorNullFulfill() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.onNext(null); - - assertThat(mp.isTerminated()).isTrue(); - assertThat(mp.peek()).isNull(); - } - - @Test - public void MonoProcessorMapFulfill() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.onNext(1); - - UnicastMonoProcessor mp2 = - mp.map(s -> s * 2).subscribeWith(UnicastMonoProcessor.create()); - mp2.subscribe(); - - assertThat(mp2.isTerminated()).isTrue(); - assertThat(mp2.peek()).isEqualTo(2); - } - - @Test - public void MonoProcessorThenFulfill() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.onNext(1); - - UnicastMonoProcessor mp2 = - mp.flatMap(s -> Mono.just(s * 2)).subscribeWith(UnicastMonoProcessor.create()); - mp2.subscribe(); - - assertThat(mp2.isTerminated()).isTrue(); - assertThat(mp2.peek()).isEqualTo(2); - } - - @Test - public void MonoProcessorMapError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.onNext(1); - - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - - StepVerifier.create( - mp.map( - s -> { - throw new RuntimeException("test"); - }) - .subscribeWith(mp2), - 0) - .thenRequest(1) - .then( - () -> { - assertThat(mp2.isTerminated()).isTrue(); - assertThat(mp2.getError()).hasMessage("test"); - }) - .verifyErrorMessage("test"); - } - - @Test(expected = Exception.class) - public void MonoProcessorDoubleError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.onError(new Exception("test")); - mp.onError(new Exception("test")); - } - - @Test(expected = Exception.class) - public void MonoProcessorDoubleSignal() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.onNext("test"); - mp.onError(new Exception("test")); - } - - @Test - public void zipMonoProcessor() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - UnicastMonoProcessor> mp3 = UnicastMonoProcessor.create(); - - StepVerifier.create(Mono.zip(mp, mp2).subscribeWith(mp3)) - .then(() -> assertThat(mp3.isTerminated()).isFalse()) - .then(() -> mp.onNext(1)) - .then(() -> assertThat(mp3.isTerminated()).isFalse()) - .then(() -> mp2.onNext(2)) - .then( - () -> { - assertThat(mp3.isTerminated()).isTrue(); - assertThat(mp3.peek().getT1()).isEqualTo(1); - assertThat(mp3.peek().getT2()).isEqualTo(2); - }) - .expectNextMatches(t -> t.getT1() == 1 && t.getT2() == 2) - .verifyComplete(); - } - - @Test - public void zipMonoProcessor2() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp3 = UnicastMonoProcessor.create(); - - StepVerifier.create(Mono.zip(d -> (Integer) d[0], mp).subscribeWith(mp3)) - .then(() -> assertThat(mp3.isTerminated()).isFalse()) - .then(() -> mp.onNext(1)) - .then( - () -> { - assertThat(mp3.isTerminated()).isTrue(); - assertThat(mp3.peek()).isEqualTo(1); - }) - .expectNext(1) - .verifyComplete(); - } - - @Test - public void zipMonoProcessorRejected() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - UnicastMonoProcessor> mp3 = UnicastMonoProcessor.create(); - - StepVerifier.create(Mono.zip(mp, mp2).subscribeWith(mp3)) - .then(() -> assertThat(mp3.isTerminated()).isFalse()) - .then(() -> mp.onError(new Exception("test"))) - .then( - () -> { - assertThat(mp3.isTerminated()).isTrue(); - assertThat(mp3.getError()).hasMessage("test"); - }) - .verifyErrorMessage("test"); - } - - @Test - public void filterMonoProcessor() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - StepVerifier.create(mp.filter(s -> s % 2 == 0).subscribeWith(mp2)) - .then(() -> mp.onNext(2)) - .then(() -> assertThat(mp2.isError()).isFalse()) - .then(() -> assertThat(mp2.isTerminated()).isTrue()) - .then(() -> assertThat(mp2.peek()).isEqualTo(2)) - .then(() -> assertThat(mp2.isTerminated()).isTrue()) - .expectNext(2) - .verifyComplete(); - } - - @Test - public void filterMonoProcessorNot() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - StepVerifier.create(mp.filter(s -> s % 2 == 0).subscribeWith(mp2)) - .then(() -> mp.onNext(1)) - .then(() -> assertThat(mp2.isError()).isFalse()) - .then(() -> assertThat(mp2.isTerminated()).isTrue()) - .then(() -> assertThat(mp2.peek()).isNull()) - .then(() -> assertThat(mp2.isTerminated()).isTrue()) - .verifyComplete(); - } - - @Test - public void filterMonoProcessorError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - StepVerifier.create( - mp.filter( - s -> { - throw new RuntimeException("test"); - }) - .subscribeWith(mp2)) - .then(() -> mp.onNext(2)) - .then(() -> assertThat(mp2.isError()).isTrue()) - .then(() -> assertThat(mp2.isTerminated()).isTrue()) - .then(() -> assertThat(mp2.getError()).hasMessage("test")) - .then(() -> assertThat(mp2.isTerminated()).isTrue()) - .verifyErrorMessage("test"); - } - - @Test - public void doOnSuccessMonoProcessorError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - StepVerifier.create( - mp.doOnSuccess( - s -> { - throw new RuntimeException("test"); - }) - .doOnError(ref::set) - .subscribeWith(mp2)) - .then(() -> mp.onNext(2)) - .then(() -> assertThat(mp2.isError()).isTrue()) - .then(() -> assertThat(ref.get()).hasMessage("test")) - .then(() -> assertThat(mp2.isTerminated()).isTrue()) - .then(() -> assertThat(mp2.getError()).hasMessage("test")) - .then(() -> assertThat(mp2.isTerminated()).isTrue()) - .verifyErrorMessage("test"); - } - - @Test - public void fluxCancelledByMonoProcessor() { - AtomicLong cancelCounter = new AtomicLong(); - Flux.range(1, 10) - .doOnCancel(cancelCounter::incrementAndGet) - .transformDeferred((s) -> s.subscribeWith(UnicastMonoProcessor.create())) - .subscribe(); - - assertThat(cancelCounter.get()).isEqualTo(1); - } - - @Test - public void cancelledByMonoProcessor() { - AtomicLong cancelCounter = new AtomicLong(); - UnicastMonoProcessor monoProcessor = - Mono.just("foo") - .doOnCancel(cancelCounter::incrementAndGet) - .subscribeWith(UnicastMonoProcessor.create()); - monoProcessor.subscribe(); - - assertThat(cancelCounter.get()).isEqualTo(1); - } - - @Test - public void scanProcessor() { - UnicastMonoProcessor test = UnicastMonoProcessor.create(); - Subscription subscription = Operators.emptySubscription(); - test.onSubscribe(subscription); - - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - - test.onComplete(); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - } - - @Test - public void scanProcessorCancelled() { - UnicastMonoProcessor test = UnicastMonoProcessor.create(); - Subscription subscription = Operators.emptySubscription(); - test.onSubscribe(subscription); - - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - - test.cancel(); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); - } - - @Test - public void scanProcessorSubscription() { - UnicastMonoProcessor test = UnicastMonoProcessor.create(); - Subscription subscription = Operators.emptySubscription(); - test.onSubscribe(subscription); - - assertThat(test.scan(Scannable.Attr.ACTUAL)).isNull(); - assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(subscription); - } - - @Test - public void scanProcessorError() { - UnicastMonoProcessor test = UnicastMonoProcessor.create(); - Subscription subscription = Operators.emptySubscription(); - test.onSubscribe(subscription); - - test.onError(new IllegalStateException("boom")); - - assertThat(test.scan(Scannable.Attr.ERROR)).hasMessage("boom"); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - } - - @Test - public void monoToProcessorConnects() { - TestPublisher tp = TestPublisher.create(); - UnicastMonoProcessor connectedProcessor = - tp.mono().subscribeWith(UnicastMonoProcessor.create()); - - assertThat(connectedProcessor.subscription).isNotNull(); - } - - @Test - public void monoToProcessorChain() { - StepVerifier.withVirtualTime( - () -> - Mono.just("foo") - .subscribeWith(UnicastMonoProcessor.create()) - .delayElement(Duration.ofMillis(500))) - .expectSubscription() - .expectNoEvent(Duration.ofMillis(500)) - .expectNext("foo") - .verifyComplete(); - } - - @Test - public void monoToProcessorChainColdToHot() { - AtomicInteger subscriptionCount = new AtomicInteger(); - Mono coldToHot = - Mono.just("foo") - .doOnSubscribe(sub -> subscriptionCount.incrementAndGet()) - .transformDeferred(s -> s.subscribeWith(UnicastMonoProcessor.create())) - .subscribeWith(UnicastMonoProcessor.create()) // this actually subscribes - .filter(s -> s.length() < 4); - - assertThat(subscriptionCount.get()).isEqualTo(1); - - coldToHot.block(); - assertThatThrownBy(coldToHot::block) - .hasMessage("UnicastMonoProcessor allows only a single Subscriber"); - assertThatThrownBy(coldToHot::block) - .hasMessage("UnicastMonoProcessor allows only a single Subscriber"); - - assertThat(subscriptionCount.get()).isEqualTo(1); - } - - @Test - public void monoProcessorBlockIsUnbounded() { - long start = System.nanoTime(); - - String result = - Mono.just("foo") - .delayElement(Duration.ofMillis(500)) - .subscribeWith(UnicastMonoProcessor.create()) - .block(); - - assertThat(result).isEqualTo("foo"); - assertThat(Duration.ofNanos(System.nanoTime() - start)) - .isGreaterThanOrEqualTo(Duration.ofMillis(500)); - } - - @Test - public void monoProcessorBlockNegativeIsImmediateTimeout() { - long start = System.nanoTime(); - - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy( - () -> - Mono.just("foo") - .delayElement(Duration.ofMillis(500)) - .subscribeWith(UnicastMonoProcessor.create()) - .block(Duration.ofSeconds(-1))) - .withMessage("Timeout on blocking read for -1000 MILLISECONDS"); - - assertThat(Duration.ofNanos(System.nanoTime() - start)).isLessThan(Duration.ofMillis(500)); - } - - @Test - public void monoProcessorBlockZeroIsImmediateTimeout() { - long start = System.nanoTime(); - - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy( - () -> - Mono.just("foo") - .delayElement(Duration.ofMillis(500)) - .subscribeWith(UnicastMonoProcessor.create()) - .block(Duration.ZERO)) - .withMessage("Timeout on blocking read for 0 MILLISECONDS"); - - assertThat(Duration.ofNanos(System.nanoTime() - start)).isLessThan(Duration.ofMillis(500)); - } - - @Test - public void disposeBeforeValueSendsCancellationException() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); - AtomicReference e1 = new AtomicReference<>(); - AtomicReference e2 = new AtomicReference<>(); - AtomicReference e3 = new AtomicReference<>(); - AtomicReference late = new AtomicReference<>(); - - processor.subscribe(v -> Assertions.fail("expected first subscriber to error"), e1::set); - processor.subscribe(v -> Assertions.fail("expected second subscriber to error"), e2::set); - processor.subscribe(v -> Assertions.fail("expected third subscriber to error"), e3::set); - - processor.dispose(); - - assertThat(e1.get()).isInstanceOf(CancellationException.class); - assertThat(e2.get()).isInstanceOf(IllegalStateException.class); - assertThat(e3.get()).isInstanceOf(IllegalStateException.class); - - processor.subscribe(v -> Assertions.fail("expected late subscriber to error"), late::set); - assertThat(late.get()).isInstanceOf(IllegalStateException.class); - } -} From 71b73dc1b4c200fbc1b8514f7c2a09f3648ecba5 Mon Sep 17 00:00:00 2001 From: Jacky Chan Date: Mon, 4 Nov 2019 22:47:06 -0800 Subject: [PATCH 109/181] Fix IMAGE_GIG naming error (#710) Signed-off-by: linux_china --- .../src/main/java/io/rsocket/metadata/WellKnownMimeType.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java index 21c2daf93..82cce54a0 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java @@ -50,7 +50,7 @@ public enum WellKnownMimeType { AUDIO_OPUS("audio/opus", (byte) 0x12), AUDIO_VORBIS("audio/vorbis", (byte) 0x13), IMAGE_BMP("image/bmp", (byte) 0x14), - IMAGE_GIG("image/gif", (byte) 0x15), + IMAGE_GIF("image/gif", (byte) 0x15), IMAGE_HEIC_SEQUENCE("image/heic-sequence", (byte) 0x16), IMAGE_HEIC("image/heic", (byte) 0x17), IMAGE_HEIF_SEQUENCE("image/heif-sequence", (byte) 0x18), From 2f36a7c46ac58aa0590e80f3b02373b4bb376a04 Mon Sep 17 00:00:00 2001 From: Franz Becker Date: Thu, 14 Nov 2019 15:29:43 +0000 Subject: [PATCH 110/181] Remove rsocket-examples from generated BOM. (#715) Fixes #714 Signed-off-by: Franz Becker --- rsocket-bom/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-bom/build.gradle b/rsocket-bom/build.gradle index ca48a87c0..0a7cda402 100755 --- a/rsocket-bom/build.gradle +++ b/rsocket-bom/build.gradle @@ -24,7 +24,7 @@ description = 'RSocket Java Bill of materials.' dependencies { constraints { - parent.subprojects.findAll { it.name != project.name }.sort { "$it.name" }.each { + parent.subprojects.findAll { it.name != project.name && it.name != "rsocket-examples" }.sort { "$it.name" }.each { api it } } From cef8711ee20dc9bd040e15d3e5f79e32319cd9f5 Mon Sep 17 00:00:00 2001 From: Sergey Tselovalnikov Date: Sun, 1 Dec 2019 02:53:42 +1100 Subject: [PATCH 111/181] remove unnecessary synchronized from default decoder (#721) Signed-off-by: Sergey Tselovalnikov --- .../java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java b/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java index 74186f1d1..692dcb363 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java @@ -11,7 +11,7 @@ class DefaultPayloadDecoder implements PayloadDecoder { @Override - public synchronized Payload apply(ByteBuf byteBuf) { + public Payload apply(ByteBuf byteBuf) { ByteBuf m; ByteBuf d; FrameType type = FrameHeaderFlyweight.frameType(byteBuf); From 5d383fa9b59ec06fdee6bfc6fa26d63493a7827f Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 5 Dec 2019 11:38:24 +0200 Subject: [PATCH 112/181] provides refactoring of benchmarks (#722) * moves all the benchmarks to a separate submodule; * provides an ability to run baseline comparison benches Signed-off-by: Oleh Dokuka --- benchmarks/README.md | 47 +++++ benchmarks/build.gradle | 163 ++++++++++++++++++ .../java/io/rsocket/MaxPerfSubscriber.java | 0 .../io/rsocket/PayloadsPerfSubscriber.java | 16 ++ .../main}/java/io/rsocket/PerfSubscriber.java | 7 +- .../main}/java/io/rsocket/RSocketPerf.java | 44 ++++- .../java/io/rsocket/StreamIdSupplierPerf.java | 0 .../frame/FrameHeaderFlyweightPerf.java | 0 .../java/io/rsocket/frame/FrameTypePerf.java | 0 .../rsocket/frame/PayloadFlyweightPerf.java | 0 .../metadata/WellKnownMimeTypePerf.java | 0 gradle.properties | 1 + gradle/wrapper/gradle-wrapper.properties | 2 +- rsocket-core/build.gradle | 4 +- rsocket-core/jmh.gradle | 46 ----- settings.gradle | 4 +- 16 files changed, 270 insertions(+), 64 deletions(-) create mode 100644 benchmarks/README.md create mode 100644 benchmarks/build.gradle rename {rsocket-core/src/jmh => benchmarks/src/main}/java/io/rsocket/MaxPerfSubscriber.java (100%) create mode 100644 benchmarks/src/main/java/io/rsocket/PayloadsPerfSubscriber.java rename {rsocket-core/src/jmh => benchmarks/src/main}/java/io/rsocket/PerfSubscriber.java (78%) rename {rsocket-core/src/jmh => benchmarks/src/main}/java/io/rsocket/RSocketPerf.java (73%) rename {rsocket-core/src/jmh => benchmarks/src/main}/java/io/rsocket/StreamIdSupplierPerf.java (100%) rename {rsocket-core/src/jmh => benchmarks/src/main}/java/io/rsocket/frame/FrameHeaderFlyweightPerf.java (100%) rename {rsocket-core/src/jmh => benchmarks/src/main}/java/io/rsocket/frame/FrameTypePerf.java (100%) rename {rsocket-core/src/jmh => benchmarks/src/main}/java/io/rsocket/frame/PayloadFlyweightPerf.java (100%) rename {rsocket-core/src/jmh => benchmarks/src/main}/java/io/rsocket/metadata/WellKnownMimeTypePerf.java (100%) delete mode 100644 rsocket-core/jmh.gradle diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 000000000..6ba6755a6 --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,47 @@ +## Usage of JMH tasks + +Only execute specific benchmark(s) (wildcards are added before and after): +``` +../gradlew jmh --include="(BenchmarkPrimary|OtherBench)" +``` +If you want to specify the wildcards yourself, you can pass the full regexp: +``` +../gradlew jmh --fullInclude=.*MyBenchmark.* +``` + +Specify extra profilers: +``` +../gradlew jmh --profilers="gc,stack" +``` + +Prominent profilers (for full list call `jmhProfilers` task): +- comp - JitCompilations, tune your iterations +- stack - which methods used most time +- gc - print garbage collection stats +- hs_thr - thread usage + +Change report format from JSON to one of [CSV, JSON, NONE, SCSV, TEXT]: +``` +./gradlew jmh --format=csv +``` + +Specify JVM arguments: +``` +../gradlew jmh --jvmArgs="-Dtest.cluster=local" +``` + +Run in verification mode (execute benchmarks with minimum of fork/warmup-/benchmark-iterations): +``` +../gradlew jmh --verify=true +``` + +## Comparing with the baseline +If you wish you run two sets of benchmarks, one for the current change and another one for the "baseline", +there is an additional task `jmhBaseline` that will use the latest release: +``` +../gradlew jmh jmhBaseline --include=MyBenchmark +``` + +## Resources +- http://tutorials.jenkov.com/java-performance/jmh.html (Introduction) +- http://hg.openjdk.java.net/code-tools/jmh/file/tip/jmh-samples/src/main/java/org/openjdk/jmh/samples/ (Samples) diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle new file mode 100644 index 000000000..fa1d6e04b --- /dev/null +++ b/benchmarks/build.gradle @@ -0,0 +1,163 @@ +apply plugin: 'java' +apply plugin: 'idea' + +configurations { + current + baseline { + resolutionStrategy.cacheChangingModulesFor 0, 'seconds' + } +} + +dependencies { + // Use the baseline to avoid using new APIs in the benchmarks + compileOnly "io.rsocket:rsocket-core:${perfBaselineVersion}" + compileOnly "io.rsocket:rsocket-transport-local:${perfBaselineVersion}" + + implementation "org.openjdk.jmh:jmh-core:1.21" + annotationProcessor "org.openjdk.jmh:jmh-generator-annprocess:1.21" + + current project(':rsocket-core') + current project(':rsocket-transport-local') + baseline "io.rsocket:rsocket-core:${perfBaselineVersion}", { + changing = true + } + baseline "io.rsocket:rsocket-transport-local:${perfBaselineVersion}", { + changing = true + } +} + +task jmhProfilers(type: JavaExec, description:'Lists the available profilers for the jmh task', group: 'Development') { + classpath = sourceSets.main.runtimeClasspath + main = 'org.openjdk.jmh.Main' + args '-lprof' +} + +task jmh(type: JmhExecTask, description: 'Executing JMH benchmarks') { + classpath = sourceSets.main.runtimeClasspath + configurations.current +} + +task jmhBaseline(type: JmhExecTask, description: 'Executing JMH baseline benchmarks') { + classpath = sourceSets.main.runtimeClasspath + configurations.baseline +} + +class JmhExecTask extends JavaExec { + + private String include; + private String fullInclude; + private String exclude; + private String format = "json"; + private String profilers; + private String jmhJvmArgs; + private String verify; + + public JmhExecTask() { + super(); + } + + public String getInclude() { + return include; + } + + @Option(option = "include", description="configure bench inclusion using substring") + public void setInclude(String include) { + this.include = include; + } + + public String getFullInclude() { + return fullInclude; + } + + @Option(option = "fullInclude", description = "explicitly configure bench inclusion using full JMH style regexp") + public void setFullInclude(String fullInclude) { + this.fullInclude = fullInclude; + } + + public String getExclude() { + return exclude; + } + + @Option(option = "exclude", description = "explicitly configure bench exclusion using full JMH style regexp") + public void setExclude(String exclude) { + this.exclude = exclude; + } + + public String getFormat() { + return format; + } + + @Option(option = "format", description = "configure report format") + public void setFormat(String format) { + this.format = format; + } + + public String getProfilers() { + return profilers; + } + + @Option(option = "profilers", description = "configure jmh profiler(s) to use, comma separated") + public void setProfilers(String profilers) { + this.profilers = profilers; + } + + public String getJmhJvmArgs() { + return jmhJvmArgs; + } + + @Option(option = "jvmArgs", description = "configure additional JMH JVM arguments, comma separated") + public void setJmhJvmArgs(String jvmArgs) { + this.jmhJvmArgs = jvmArgs; + } + + public String getVerify() { + return verify; + } + + @Option(option = "verify", description = "run in verify mode") + public void setVerify(String verify) { + this.verify = verify; + } + + @TaskAction + public void exec() { + setMain("org.openjdk.jmh.Main"); + File resultFile = getProject().file("build/reports/" + getName() + "/result." + format); + + if (include != null) { + args(".*" + include + ".*"); + } + else if (fullInclude != null) { + args(fullInclude); + } + + if(exclude != null) { + args("-e", exclude); + } + if(verify != null) { // execute benchmarks with the minimum amount of execution (only to check if they are working) + System.out.println("Running in verify mode"); + args("-f", 1); + args("-wi", 1); + args("-i", 1); + } + args("-foe", "true"); //fail-on-error + args("-v", "NORMAL"); //verbosity [SILENT, NORMAL, EXTRA] + if(profilers != null) { + for (String prof : profilers.split(",")) { + args("-prof", prof); + } + } + args("-jvmArgsPrepend", "-Xmx3072m"); + args("-jvmArgsPrepend", "-Xms3072m"); + if(jmhJvmArgs != null) { + for(String jvmArg : jmhJvmArgs.split(" ")) { + args("-jvmArgsPrepend", jvmArg); + } + } + args("-rf", format); + args("-rff", resultFile); + + System.out.println("\nExecuting JMH with: " + getArgs() + "\n"); + resultFile.getParentFile().mkdirs(); + + super.exec(); + } +} \ No newline at end of file diff --git a/rsocket-core/src/jmh/java/io/rsocket/MaxPerfSubscriber.java b/benchmarks/src/main/java/io/rsocket/MaxPerfSubscriber.java similarity index 100% rename from rsocket-core/src/jmh/java/io/rsocket/MaxPerfSubscriber.java rename to benchmarks/src/main/java/io/rsocket/MaxPerfSubscriber.java diff --git a/benchmarks/src/main/java/io/rsocket/PayloadsPerfSubscriber.java b/benchmarks/src/main/java/io/rsocket/PayloadsPerfSubscriber.java new file mode 100644 index 000000000..efc116958 --- /dev/null +++ b/benchmarks/src/main/java/io/rsocket/PayloadsPerfSubscriber.java @@ -0,0 +1,16 @@ +package io.rsocket; + +import org.openjdk.jmh.infra.Blackhole; + +public class PayloadsPerfSubscriber extends PerfSubscriber { + + public PayloadsPerfSubscriber(Blackhole blackhole) { + super(blackhole); + } + + @Override + public void onNext(Payload payload) { + payload.release(); + super.onNext(payload); + } +} diff --git a/rsocket-core/src/jmh/java/io/rsocket/PerfSubscriber.java b/benchmarks/src/main/java/io/rsocket/PerfSubscriber.java similarity index 78% rename from rsocket-core/src/jmh/java/io/rsocket/PerfSubscriber.java rename to benchmarks/src/main/java/io/rsocket/PerfSubscriber.java index 98c5edd3b..5177c1e29 100644 --- a/rsocket-core/src/jmh/java/io/rsocket/PerfSubscriber.java +++ b/benchmarks/src/main/java/io/rsocket/PerfSubscriber.java @@ -5,9 +5,9 @@ import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; -public class PerfSubscriber implements CoreSubscriber { +public class PerfSubscriber implements CoreSubscriber { - final CountDownLatch latch = new CountDownLatch(1); + public final CountDownLatch latch = new CountDownLatch(1); final Blackhole blackhole; Subscription s; @@ -23,8 +23,7 @@ public void onSubscribe(Subscription s) { } @Override - public void onNext(Payload payload) { - payload.release(); + public void onNext(T payload) { blackhole.consume(payload); s.request(1); } diff --git a/rsocket-core/src/jmh/java/io/rsocket/RSocketPerf.java b/benchmarks/src/main/java/io/rsocket/RSocketPerf.java similarity index 73% rename from rsocket-core/src/jmh/java/io/rsocket/RSocketPerf.java rename to benchmarks/src/main/java/io/rsocket/RSocketPerf.java index 476d6c814..746524f87 100644 --- a/rsocket-core/src/jmh/java/io/rsocket/RSocketPerf.java +++ b/benchmarks/src/main/java/io/rsocket/RSocketPerf.java @@ -4,15 +4,20 @@ import io.rsocket.transport.local.LocalClientTransport; import io.rsocket.transport.local.LocalServerTransport; import io.rsocket.util.EmptyPayload; +import java.lang.reflect.Field; +import java.util.Queue; +import java.util.concurrent.locks.LockSupport; import java.util.stream.IntStream; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; import org.reactivestreams.Publisher; @@ -36,11 +41,26 @@ public class RSocketPerf { RSocket client; Closeable server; + Queue clientsQueue; + + @TearDown + public void tearDown() { + client.dispose(); + server.dispose(); + } + + @TearDown(Level.Iteration) + public void awaitToBeConsumed() { + while (!clientsQueue.isEmpty()) { + LockSupport.parkNanos(1000); + } + } @Setup - public void setUp() { + public void setUp() throws NoSuchFieldException, IllegalAccessException { server = RSocketFactory.receive() + .frameDecoder(PayloadDecoder.ZERO_COPY) .acceptor( (setup, sendingSocket) -> Mono.just( @@ -75,16 +95,22 @@ public Flux requestChannel(Publisher payloads) { client = RSocketFactory.connect() + .singleSubscriberRequester() .frameDecoder(PayloadDecoder.ZERO_COPY) .transport(LocalClientTransport.create("server")) .start() .block(); + + Field sendProcessorField = RSocketRequester.class.getDeclaredField("sendProcessor"); + sendProcessorField.setAccessible(true); + + clientsQueue = (Queue) sendProcessorField.get(client); } @Benchmark @SuppressWarnings("unchecked") - public PerfSubscriber fireAndForget(Blackhole blackhole) throws InterruptedException { - PerfSubscriber subscriber = new PerfSubscriber(blackhole); + public PayloadsPerfSubscriber fireAndForget(Blackhole blackhole) throws InterruptedException { + PayloadsPerfSubscriber subscriber = new PayloadsPerfSubscriber(blackhole); client.fireAndForget(PAYLOAD).subscribe((CoreSubscriber) subscriber); subscriber.latch.await(); @@ -92,8 +118,8 @@ public PerfSubscriber fireAndForget(Blackhole blackhole) throws InterruptedExcep } @Benchmark - public PerfSubscriber requestResponse(Blackhole blackhole) throws InterruptedException { - PerfSubscriber subscriber = new PerfSubscriber(blackhole); + public PayloadsPerfSubscriber requestResponse(Blackhole blackhole) throws InterruptedException { + PayloadsPerfSubscriber subscriber = new PayloadsPerfSubscriber(blackhole); client.requestResponse(PAYLOAD).subscribe(subscriber); subscriber.latch.await(); @@ -101,9 +127,9 @@ public PerfSubscriber requestResponse(Blackhole blackhole) throws InterruptedExc } @Benchmark - public PerfSubscriber requestStreamWithRequestByOneStrategy(Blackhole blackhole) + public PayloadsPerfSubscriber requestStreamWithRequestByOneStrategy(Blackhole blackhole) throws InterruptedException { - PerfSubscriber subscriber = new PerfSubscriber(blackhole); + PayloadsPerfSubscriber subscriber = new PayloadsPerfSubscriber(blackhole); client.requestStream(PAYLOAD).subscribe(subscriber); subscriber.latch.await(); @@ -121,9 +147,9 @@ public MaxPerfSubscriber requestStreamWithRequestAllStrategy(Blackhole blackhole } @Benchmark - public PerfSubscriber requestChannelWithRequestByOneStrategy(Blackhole blackhole) + public PayloadsPerfSubscriber requestChannelWithRequestByOneStrategy(Blackhole blackhole) throws InterruptedException { - PerfSubscriber subscriber = new PerfSubscriber(blackhole); + PayloadsPerfSubscriber subscriber = new PayloadsPerfSubscriber(blackhole); client.requestChannel(PAYLOAD_FLUX).subscribe(subscriber); subscriber.latch.await(); diff --git a/rsocket-core/src/jmh/java/io/rsocket/StreamIdSupplierPerf.java b/benchmarks/src/main/java/io/rsocket/StreamIdSupplierPerf.java similarity index 100% rename from rsocket-core/src/jmh/java/io/rsocket/StreamIdSupplierPerf.java rename to benchmarks/src/main/java/io/rsocket/StreamIdSupplierPerf.java diff --git a/rsocket-core/src/jmh/java/io/rsocket/frame/FrameHeaderFlyweightPerf.java b/benchmarks/src/main/java/io/rsocket/frame/FrameHeaderFlyweightPerf.java similarity index 100% rename from rsocket-core/src/jmh/java/io/rsocket/frame/FrameHeaderFlyweightPerf.java rename to benchmarks/src/main/java/io/rsocket/frame/FrameHeaderFlyweightPerf.java diff --git a/rsocket-core/src/jmh/java/io/rsocket/frame/FrameTypePerf.java b/benchmarks/src/main/java/io/rsocket/frame/FrameTypePerf.java similarity index 100% rename from rsocket-core/src/jmh/java/io/rsocket/frame/FrameTypePerf.java rename to benchmarks/src/main/java/io/rsocket/frame/FrameTypePerf.java diff --git a/rsocket-core/src/jmh/java/io/rsocket/frame/PayloadFlyweightPerf.java b/benchmarks/src/main/java/io/rsocket/frame/PayloadFlyweightPerf.java similarity index 100% rename from rsocket-core/src/jmh/java/io/rsocket/frame/PayloadFlyweightPerf.java rename to benchmarks/src/main/java/io/rsocket/frame/PayloadFlyweightPerf.java diff --git a/rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java b/benchmarks/src/main/java/io/rsocket/metadata/WellKnownMimeTypePerf.java similarity index 100% rename from rsocket-core/src/jmh/java/io/rsocket/metadata/WellKnownMimeTypePerf.java rename to benchmarks/src/main/java/io/rsocket/metadata/WellKnownMimeTypePerf.java diff --git a/gradle.properties b/gradle.properties index b85cba325..13a89e30c 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,3 +12,4 @@ # limitations under the License. # version=1.0.0-RC6 +perfBaselineVersion=1.0.0-RC5 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7c4388a92..f04d6a20a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index d62452619..ca2ae0e65 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -46,6 +46,4 @@ dependencies { testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' } -description = "Core functionality for the RSocket library" - -apply from: 'jmh.gradle' +description = "Core functionality for the RSocket library" \ No newline at end of file diff --git a/rsocket-core/jmh.gradle b/rsocket-core/jmh.gradle deleted file mode 100644 index 2a2b4d7cd..000000000 --- a/rsocket-core/jmh.gradle +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2015-2018 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. - */ - -dependencies { - jmh configurations.api - jmh configurations.implementation - jmh 'org.openjdk.jmh:jmh-core' - jmh 'org.openjdk.jmh:jmh-generator-annprocess' - jmh 'io.projectreactor:reactor-test' - jmh project(':rsocket-transport-local') -} - -jmhCompileGeneratedClasses.enabled = false - -jmh { - includeTests = false - profilers = ['gc'] - resultFormat = 'JSON' - - jvmArgs = ['-XX:+UnlockCommercialFeatures', '-XX:+FlightRecorder'] - // jvmArgsAppend = ['-XX:+UseG1GC', '-Xms4g', '-Xmx4g'] -} - -jmhJar { - from project.configurations.jmh -} - -tasks.jmh.finalizedBy tasks.jmhReport - -jmhReport { - jmhResultPath = project.file('build/reports/jmh/results.json') - jmhReportOutput = project.file('build/reports/jmh') -} diff --git a/settings.gradle b/settings.gradle index 625633774..c88d23bbe 100644 --- a/settings.gradle +++ b/settings.gradle @@ -17,10 +17,12 @@ rootProject.name = 'rsocket-java' include 'rsocket-core' -include 'rsocket-examples' include 'rsocket-load-balancer' include 'rsocket-micrometer' include 'rsocket-test' include 'rsocket-transport-local' include 'rsocket-transport-netty' include 'rsocket-bom' + +include 'rsocket-examples' +include 'benchmarks' From 4f44695a53d02272be3a179ae273f4894f4dc6a9 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 5 Dec 2019 16:12:41 +0200 Subject: [PATCH 113/181] provides build number for PRs (#724) * provides build number for PRs Signed-off-by: Oleh Dokuka * updates jdk-12 to jdk-13 build Signed-off-by: Oleh Dokuka * updates to gradle 6+ Signed-off-by: Oleh Dokuka * fixes issues related to migration to gradle 6 Signed-off-by: Oleh Dokuka * provides formatting Signed-off-by: Oleh Dokuka --- .travis.yml | 2 +- build.gradle | 11 ++++----- ci/travis.sh | 16 +++++++++++-- gradle/artifactory.gradle | 4 ++++ gradle/wrapper/gradle-wrapper.jar | Bin 56177 -> 55616 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 22 +++++++++++++++--- gradlew.bat | 18 +++++++++++++- rsocket-bom/build.gradle | 6 ----- .../java/io/rsocket/DuplexConnection.java | 20 ++++++++-------- settings.gradle | 13 +++++++++++ 11 files changed, 83 insertions(+), 31 deletions(-) diff --git a/.travis.yml b/.travis.yml index 116d2d2ff..a40bdf55e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ matrix: - jdk: openjdk8 - jdk: openjdk11 env: SKIP_RELEASE=true - - jdk: openjdk12 + - jdk: openjdk13 env: SKIP_RELEASE=true env: diff --git a/build.gradle b/build.gradle index c354869df..e834e21bd 100644 --- a/build.gradle +++ b/build.gradle @@ -15,13 +15,11 @@ */ plugins { - id 'com.gradle.build-scan' version '2.4.2' - id 'com.github.sherter.google-java-format' version '0.8' apply false - id 'com.jfrog.artifactory' version '4.9.10' apply false + id 'com.jfrog.artifactory' version '4.11.0' apply false id 'com.jfrog.bintray' version '1.8.4' apply false - id 'me.champeau.gradle.jmh' version '0.4.8' apply false - id 'io.spring.dependency-management' version '1.0.7.RELEASE' apply false + id 'me.champeau.gradle.jmh' version '0.5.0' apply false + id 'io.spring.dependency-management' version '1.0.8.RELEASE' apply false id 'io.morethan.jmhreport' version '0.9.0' apply false } @@ -35,7 +33,7 @@ subprojects { ext['netty-bom.version'] = '4.1.37.Final' ext['netty-boringssl.version'] = '2.0.25.Final' ext['hdrhistogram.version'] = '2.1.10' - ext['mockito.version'] = '2.25.1' + ext['mockito.version'] = '3.2.0' ext['slf4j.version'] = '1.7.25' ext['jmh.version'] = '1.21' ext['junit.version'] = '5.5.2' @@ -145,7 +143,6 @@ subprojects { } } } - } apply from: "${rootDir}/gradle/publications.gradle" diff --git a/ci/travis.sh b/ci/travis.sh index 9154da33b..411f4418b 100755 --- a/ci/travis.sh +++ b/ci/travis.sh @@ -5,13 +5,24 @@ if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then echo -e "Building PR #$TRAVIS_PULL_REQUEST [$TRAVIS_PULL_REQUEST_SLUG/$TRAVIS_PULL_REQUEST_BRANCH => $TRAVIS_REPO_SLUG/$TRAVIS_BRANCH]" ./gradlew build -elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ] && [ "$bintrayUser" != "" ] ; then +elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ] && [ "$bintrayUser" != "" ] && [ "$TRAVIS_BRANCH" == "develop" ] ; then - echo -e "Building Snapshot $TRAVIS_REPO_SLUG/$TRAVIS_BRANCH" + echo -e "Building Develop Snapshot $TRAVIS_REPO_SLUG/$TRAVIS_BRANCH/$TRAVIS_BUILD_NUMBER" ./gradlew \ -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" \ -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" \ -PversionSuffix="-SNAPSHOT" \ + -PbuildNumber="$TRAVIS_BUILD_NUMBER" \ + build artifactoryPublish --stacktrace + +elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ] && [ "$bintrayUser" != "" ] ; then + + echo -e "Building Branch Snapshot $TRAVIS_REPO_SLUG/$TRAVIS_BRANCH/$TRAVIS_BUILD_NUMBER" + ./gradlew \ + -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" \ + -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" \ + -PversionSuffix="-$TRAVIS_BRANCH-SNAPSHOT" \ + -PbuildNumber="$TRAVIS_BUILD_NUMBER" \ build artifactoryPublish --stacktrace elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ] && [ "$bintrayUser" != "" ] ; then @@ -21,6 +32,7 @@ elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" != "" ] && [ "$bin -Pversion="$TRAVIS_TAG" \ -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" \ -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" \ + -PbuildNumber="$TRAVIS_BUILD_NUMBER" \ build bintrayUpload --stacktrace else diff --git a/gradle/artifactory.gradle b/gradle/artifactory.gradle index 7f4369242..cdffb2741 100644 --- a/gradle/artifactory.gradle +++ b/gradle/artifactory.gradle @@ -33,6 +33,10 @@ if (project.hasProperty('bintrayUser') && project.hasProperty('bintrayKey')) { defaults { publications(publishing.publications.maven) } + + if (project.hasProperty('buildNumber')) { + clientConfig.info.setBuildNumber(project.property('buildNumber').toString()) + } } } tasks.named("artifactoryPublish").configure { diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 29953ea141f55e3b8fc691d31b5ca8816d89fa87..5c2d1cf016b3885f6930543d57b744ea8c220a1a 100644 GIT binary patch delta 47360 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 delta 47987 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 P{tQ5B1&<&s7});Flow control + *

Flow control * - * The passed {@code Publisher} must + *

The passed {@code Publisher} must * * @param frames Stream of {@code Frame}s to send on the connection. * @return {@code Publisher} that completes when all the frames are written on the connection @@ -56,20 +56,20 @@ default Mono sendOne(ByteBuf frame) { /** * Returns a stream of all {@code Frame}s received on this connection. * - *

Completion

+ *

Completion * - * Returned {@code Publisher} MUST never emit a completion event ({@link + *

Returned {@code Publisher} MUST never emit a completion event ({@link * Subscriber#onComplete()}. * - *

Error

+ *

Error * - * Returned {@code Publisher} can error with various transport errors. If the underlying physical - * connection is closed by the peer, then the returned stream from here MUST emit an - * {@link ClosedChannelException}. + *

Returned {@code Publisher} can error with various transport errors. If the underlying + * physical connection is closed by the peer, then the returned stream from here MUST + * emit an {@link ClosedChannelException}. * - *

Multiple Subscriptions

+ *

Multiple Subscriptions * - * Returned {@code Publisher} is not required to support multiple concurrent subscriptions. + *

Returned {@code Publisher} is not required to support multiple concurrent subscriptions. * RSocket will never have multiple subscriptions to this source. Implementations MUST * emit an {@link IllegalStateException} for subsequent concurrent subscriptions, if they do not * support multiple concurrent subscriptions. diff --git a/settings.gradle b/settings.gradle index c88d23bbe..25c3feee5 100644 --- a/settings.gradle +++ b/settings.gradle @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +plugins { + id 'com.gradle.enterprise' version '3.1' +} rootProject.name = 'rsocket-java' @@ -26,3 +29,13 @@ include 'rsocket-bom' include 'rsocket-examples' include 'benchmarks' + + + +gradleEnterprise { + buildScan { + termsOfServiceUrl = 'https://gradle.com/terms-of-service' + termsOfServiceAgree = 'yes' + } +} + From b3865b90bed3ca39764531eabc2c90d368da5011 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 19 Nov 2019 13:38:35 +0200 Subject: [PATCH 114/181] exclude benchmarks subproject from BOM Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka --- rsocket-bom/build.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rsocket-bom/build.gradle b/rsocket-bom/build.gradle index 4aa5c8f03..2efc20a91 100755 --- a/rsocket-bom/build.gradle +++ b/rsocket-bom/build.gradle @@ -22,9 +22,11 @@ plugins { description = 'RSocket Java Bill of materials.' +def excluded = ["rsocket-examples", "benchmarks"] + dependencies { constraints { - parent.subprojects.findAll { it.name != project.name && it.name != "rsocket-examples" }.sort { "$it.name" }.each { + parent.subprojects.findAll { it.name != project.name && !excluded.contains(it.name) } .sort { "$it.name" }.each { api it } } From 56c8965afb0e9ca57f6179c4500b897eb440e810 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 5 Dec 2019 17:06:27 +0200 Subject: [PATCH 115/181] Feature/mono processor rework (#723) * reworking unicast mono processor impl This reverts commit c5684ebd2769ed5ba15de8dc00dea7f61fc5d30d. Signed-off-by: Oleh Dokuka * reworking unicast mono processor impl This reverts commit c5684ebd2769ed5ba15de8dc00dea7f61fc5d30d. Signed-off-by: Oleh Dokuka * provides perf comparison Signed-off-by: Oleh Dokuka * provides more strict tests Signed-off-by: Oleh Dokuka * provides formatting Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/MaxPerfSubscriber.java | 11 +- .../io/rsocket/PayloadsMaxPerfSubscriber.java | 16 + .../main/java/io/rsocket/PerfSubscriber.java | 8 +- .../src/main/java/io/rsocket/RSocketPerf.java | 20 +- .../UnicastVsDefaultMonoProcessorPerf.java | 46 + .../internal/UnicastMonoProcessor.java | 478 +++++- .../internal/UnicastMonoProcessorTest.java | 1451 +++++++++++++++++ .../internal/subscriber/AssertSubscriber.java | 1154 +++++++++++++ 8 files changed, 3074 insertions(+), 110 deletions(-) create mode 100644 benchmarks/src/main/java/io/rsocket/PayloadsMaxPerfSubscriber.java create mode 100644 benchmarks/src/main/java/io/rsocket/internal/UnicastVsDefaultMonoProcessorPerf.java create mode 100644 rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/internal/subscriber/AssertSubscriber.java diff --git a/benchmarks/src/main/java/io/rsocket/MaxPerfSubscriber.java b/benchmarks/src/main/java/io/rsocket/MaxPerfSubscriber.java index ace985a39..2e6fa6acc 100644 --- a/benchmarks/src/main/java/io/rsocket/MaxPerfSubscriber.java +++ b/benchmarks/src/main/java/io/rsocket/MaxPerfSubscriber.java @@ -5,12 +5,12 @@ import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; -public class MaxPerfSubscriber implements CoreSubscriber { +public class MaxPerfSubscriber extends CountDownLatch implements CoreSubscriber { - final CountDownLatch latch = new CountDownLatch(1); final Blackhole blackhole; public MaxPerfSubscriber(Blackhole blackhole) { + super(1); this.blackhole = blackhole; } @@ -20,19 +20,18 @@ public void onSubscribe(Subscription s) { } @Override - public void onNext(Payload payload) { - payload.release(); + public void onNext(T payload) { blackhole.consume(payload); } @Override public void onError(Throwable t) { blackhole.consume(t); - latch.countDown(); + countDown(); } @Override public void onComplete() { - latch.countDown(); + countDown(); } } diff --git a/benchmarks/src/main/java/io/rsocket/PayloadsMaxPerfSubscriber.java b/benchmarks/src/main/java/io/rsocket/PayloadsMaxPerfSubscriber.java new file mode 100644 index 000000000..7a7a1fdd6 --- /dev/null +++ b/benchmarks/src/main/java/io/rsocket/PayloadsMaxPerfSubscriber.java @@ -0,0 +1,16 @@ +package io.rsocket; + +import org.openjdk.jmh.infra.Blackhole; + +public class PayloadsMaxPerfSubscriber extends MaxPerfSubscriber { + + public PayloadsMaxPerfSubscriber(Blackhole blackhole) { + super(blackhole); + } + + @Override + public void onNext(Payload payload) { + payload.release(); + super.onNext(payload); + } +} diff --git a/benchmarks/src/main/java/io/rsocket/PerfSubscriber.java b/benchmarks/src/main/java/io/rsocket/PerfSubscriber.java index 5177c1e29..92577d95c 100644 --- a/benchmarks/src/main/java/io/rsocket/PerfSubscriber.java +++ b/benchmarks/src/main/java/io/rsocket/PerfSubscriber.java @@ -5,14 +5,14 @@ import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; -public class PerfSubscriber implements CoreSubscriber { +public class PerfSubscriber extends CountDownLatch implements CoreSubscriber { - public final CountDownLatch latch = new CountDownLatch(1); final Blackhole blackhole; Subscription s; public PerfSubscriber(Blackhole blackhole) { + super(1); this.blackhole = blackhole; } @@ -31,11 +31,11 @@ public void onNext(T payload) { @Override public void onError(Throwable t) { blackhole.consume(t); - latch.countDown(); + countDown(); } @Override public void onComplete() { - latch.countDown(); + countDown(); } } diff --git a/benchmarks/src/main/java/io/rsocket/RSocketPerf.java b/benchmarks/src/main/java/io/rsocket/RSocketPerf.java index 746524f87..0c6515140 100644 --- a/benchmarks/src/main/java/io/rsocket/RSocketPerf.java +++ b/benchmarks/src/main/java/io/rsocket/RSocketPerf.java @@ -112,7 +112,7 @@ public Flux requestChannel(Publisher payloads) { public PayloadsPerfSubscriber fireAndForget(Blackhole blackhole) throws InterruptedException { PayloadsPerfSubscriber subscriber = new PayloadsPerfSubscriber(blackhole); client.fireAndForget(PAYLOAD).subscribe((CoreSubscriber) subscriber); - subscriber.latch.await(); + subscriber.await(); return subscriber; } @@ -121,7 +121,7 @@ public PayloadsPerfSubscriber fireAndForget(Blackhole blackhole) throws Interrup public PayloadsPerfSubscriber requestResponse(Blackhole blackhole) throws InterruptedException { PayloadsPerfSubscriber subscriber = new PayloadsPerfSubscriber(blackhole); client.requestResponse(PAYLOAD).subscribe(subscriber); - subscriber.latch.await(); + subscriber.await(); return subscriber; } @@ -131,17 +131,17 @@ public PayloadsPerfSubscriber requestStreamWithRequestByOneStrategy(Blackhole bl throws InterruptedException { PayloadsPerfSubscriber subscriber = new PayloadsPerfSubscriber(blackhole); client.requestStream(PAYLOAD).subscribe(subscriber); - subscriber.latch.await(); + subscriber.await(); return subscriber; } @Benchmark - public MaxPerfSubscriber requestStreamWithRequestAllStrategy(Blackhole blackhole) + public PayloadsMaxPerfSubscriber requestStreamWithRequestAllStrategy(Blackhole blackhole) throws InterruptedException { - MaxPerfSubscriber subscriber = new MaxPerfSubscriber(blackhole); + PayloadsMaxPerfSubscriber subscriber = new PayloadsMaxPerfSubscriber(blackhole); client.requestStream(PAYLOAD).subscribe(subscriber); - subscriber.latch.await(); + subscriber.await(); return subscriber; } @@ -151,17 +151,17 @@ public PayloadsPerfSubscriber requestChannelWithRequestByOneStrategy(Blackhole b throws InterruptedException { PayloadsPerfSubscriber subscriber = new PayloadsPerfSubscriber(blackhole); client.requestChannel(PAYLOAD_FLUX).subscribe(subscriber); - subscriber.latch.await(); + subscriber.await(); return subscriber; } @Benchmark - public MaxPerfSubscriber requestChannelWithRequestAllStrategy(Blackhole blackhole) + public PayloadsMaxPerfSubscriber requestChannelWithRequestAllStrategy(Blackhole blackhole) throws InterruptedException { - MaxPerfSubscriber subscriber = new MaxPerfSubscriber(blackhole); + PayloadsMaxPerfSubscriber subscriber = new PayloadsMaxPerfSubscriber(blackhole); client.requestChannel(PAYLOAD_FLUX).subscribe(subscriber); - subscriber.latch.await(); + subscriber.await(); return subscriber; } diff --git a/benchmarks/src/main/java/io/rsocket/internal/UnicastVsDefaultMonoProcessorPerf.java b/benchmarks/src/main/java/io/rsocket/internal/UnicastVsDefaultMonoProcessorPerf.java new file mode 100644 index 000000000..963067ef0 --- /dev/null +++ b/benchmarks/src/main/java/io/rsocket/internal/UnicastVsDefaultMonoProcessorPerf.java @@ -0,0 +1,46 @@ +package io.rsocket.internal; + +import io.rsocket.MaxPerfSubscriber; +import java.util.concurrent.TimeUnit; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; +import reactor.core.publisher.MonoProcessor; + +@BenchmarkMode({Mode.Throughput, Mode.SampleTime}) +@Fork(1) +@Warmup(iterations = 10) +@Measurement(iterations = 10, time = 20) +@State(Scope.Benchmark) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class UnicastVsDefaultMonoProcessorPerf { + + @Benchmark + public void monoProcessorPerf(Blackhole bh) { + MaxPerfSubscriber subscriber = new MaxPerfSubscriber<>(bh); + MonoProcessor monoProcessor = MonoProcessor.create(); + monoProcessor.onNext(1); + monoProcessor.subscribe(subscriber); + + bh.consume(monoProcessor); + bh.consume(subscriber); + } + + @Benchmark + public void unicastMonoProcessorPerf(Blackhole bh) { + MaxPerfSubscriber subscriber = new MaxPerfSubscriber<>(bh); + UnicastMonoProcessor monoProcessor = UnicastMonoProcessor.create(); + monoProcessor.onNext(1); + monoProcessor.subscribe(subscriber); + + bh.consume(monoProcessor); + bh.consume(subscriber); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java index 35d4906ec..afe72c478 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java @@ -1,179 +1,477 @@ +/* + * Copyright 2015-2019 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.internal; import java.util.Objects; +import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.stream.Stream; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import org.reactivestreams.Processor; +import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; import reactor.core.Disposable; +import reactor.core.Exceptions; import reactor.core.Scannable; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Operators; +import reactor.util.annotation.NonNull; import reactor.util.annotation.Nullable; import reactor.util.context.Context; -import reactor.util.function.Tuple2; public class UnicastMonoProcessor extends Mono implements Processor, CoreSubscriber, Disposable, Subscription, Scannable { + /** + * Create a {@link UnicastMonoProcessor} that will eagerly request 1 on {@link + * #onSubscribe(Subscription)}, cache and emit the eventual result for 1 or N subscribers. + * + * @param type of the expected value + * @return A {@link UnicastMonoProcessor}. + */ + public static UnicastMonoProcessor create() { + return new UnicastMonoProcessor<>(); + } + + /** Indicates this Subscription has no value and not requested yet. */ + static final int NO_SUBSCRIBER_NO_RESULT = 0; + /** Indicates this Subscription has no value and not requested yet. */ + static final int NO_SUBSCRIBER_HAS_RESULT = 1; + /** Indicates this Subscription has no value and not requested yet. */ + static final int NO_REQUEST_NO_RESULT = 4; + /** Indicates this Subscription has a value but not requested yet. */ + static final int NO_REQUEST_HAS_RESULT = 5; + /** Indicates this Subscription has been requested but there is no value yet. */ + static final int HAS_REQUEST_NO_RESULT = 6; + /** Indicates this Subscription has both request and value. */ + static final int HAS_REQUEST_HAS_RESULT = 7; + /** Indicates the Subscription has been cancelled. */ + static final int CANCELLED = 8; + + volatile int state; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater STATE = + AtomicIntegerFieldUpdater.newUpdater(UnicastMonoProcessor.class, "state"); + + volatile int once; + @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater ONCE = AtomicIntegerFieldUpdater.newUpdater(UnicastMonoProcessor.class, "once"); - private final MonoProcessor processor; + volatile Subscription subscription; - @SuppressWarnings("unused") - private volatile int once; + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater UPSTREAM = + AtomicReferenceFieldUpdater.newUpdater( + UnicastMonoProcessor.class, Subscription.class, "subscription"); - private UnicastMonoProcessor() { - this.processor = MonoProcessor.create(); - } + CoreSubscriber actual; - public static UnicastMonoProcessor create() { - return new UnicastMonoProcessor<>(); - } + Throwable error; + O value; - @Override - public Stream actuals() { - return processor.actuals(); - } + UnicastMonoProcessor() {} @Override - public boolean isScanAvailable() { - return processor.isScanAvailable(); + @NonNull + public Context currentContext() { + final CoreSubscriber a = this.actual; + return a != null ? a.currentContext() : Context.empty(); } @Override - public String name() { - return processor.name(); + public final void onSubscribe(Subscription subscription) { + if (Operators.setOnce(UPSTREAM, this, subscription)) { + subscription.request(Long.MAX_VALUE); + } } @Override - public String stepName() { - return processor.stepName(); + public final void onComplete() { + onNext(null); } @Override - public Stream steps() { - return processor.steps(); + public final void onError(Throwable cause) { + Objects.requireNonNull(cause, "onError cannot be null"); + + if (UPSTREAM.getAndSet(this, Operators.cancelledSubscription()) + == Operators.cancelledSubscription()) { + Operators.onErrorDropped(cause, currentContext()); + return; + } + + complete(cause); } @Override - public Stream parents() { - return processor.parents(); + public final void onNext(@Nullable O value) { + final Subscription s; + if ((s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription())) + == Operators.cancelledSubscription()) { + if (value != null) { + Operators.onNextDropped(value, currentContext()); + } + return; + } + + if (value == null) { + complete(); + } else { + if (s != null) { + s.cancel(); + } + + complete(value); + } } - @Override - @Nullable - public T scan(Attr key) { - return processor.scan(key); + /** + * Tries to emit the value and complete the underlying subscriber or stores the value away until + * there is a request for it. + * + *

Make sure this method is called at most once + * + * @param v the value to emit + */ + private void complete(O v) { + for (; ; ) { + int state = this.state; + + // if state is >= HAS_CANCELLED or bit zero is set (*_HAS_VALUE) case, return + if ((state & ~HAS_REQUEST_NO_RESULT) != 0) { + this.value = null; + Operators.onDiscard(v, currentContext()); + return; + } + + if (state == HAS_REQUEST_NO_RESULT) { + final Subscriber a = actual; + if (STATE.compareAndSet(this, HAS_REQUEST_NO_RESULT, HAS_REQUEST_HAS_RESULT)) { + this.value = null; + a.onNext(v); + a.onComplete(); + return; + } + } + setValue(v); + if (state == NO_REQUEST_NO_RESULT + && STATE.compareAndSet(this, NO_REQUEST_NO_RESULT, NO_REQUEST_HAS_RESULT)) { + return; + } + if (state == NO_SUBSCRIBER_NO_RESULT + && STATE.compareAndSet(this, NO_SUBSCRIBER_NO_RESULT, NO_SUBSCRIBER_HAS_RESULT)) { + return; + } + } } - @Override - public T scanOrDefault(Attr key, T defaultValue) { - return processor.scanOrDefault(key, defaultValue); + /** + * Tries to emit completion the underlying subscriber + * + *

Make sure this method is called at most once + */ + private void complete() { + for (; ; ) { + int state = this.state; + + // if state is >= HAS_CANCELLED or bit zero is set (*_HAS_VALUE) case, return + if ((state & ~HAS_REQUEST_NO_RESULT) != 0) { + return; + } + + if (state == HAS_REQUEST_NO_RESULT || state == NO_REQUEST_NO_RESULT) { + final Subscriber a = actual; + if (STATE.compareAndSet(this, state, HAS_REQUEST_HAS_RESULT)) { + a.onComplete(); + return; + } + } + if (state == NO_SUBSCRIBER_NO_RESULT + && STATE.compareAndSet(this, NO_SUBSCRIBER_NO_RESULT, NO_SUBSCRIBER_HAS_RESULT)) { + return; + } + } } - @Override - public Stream> tags() { - return processor.tags(); + /** + * Tries to emit error the underlying subscriber or stores the value away until there is a request + * for it. + * + *

Make sure this method is called at most once + * + * @param e the error to emit + */ + private void complete(Throwable e) { + for (; ; ) { + int state = this.state; + + // if state is >= HAS_CANCELLED or bit zero is set (*_HAS_VALUE) case, return + if ((state & ~HAS_REQUEST_NO_RESULT) != 0) { + return; + } + + setError(e); + if (state == HAS_REQUEST_NO_RESULT || state == NO_REQUEST_NO_RESULT) { + final Subscriber a = actual; + if (STATE.compareAndSet(this, state, HAS_REQUEST_HAS_RESULT)) { + a.onError(e); + return; + } + } + if (state == NO_SUBSCRIBER_NO_RESULT + && STATE.compareAndSet(this, NO_SUBSCRIBER_NO_RESULT, NO_SUBSCRIBER_HAS_RESULT)) { + return; + } + } } @Override - public void onSubscribe(Subscription s) { - processor.onSubscribe(s); + public void subscribe(CoreSubscriber actual) { + Objects.requireNonNull(actual, "subscribe"); + + if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { + this.actual = actual; + + // CAS LOOP since there is a racing with [onNext / onComplete / onError / dispose] which can + // appear at any moment + + int state = this.state; + + // possible states within the racing between [onNext / onComplete / onError / dispose] and + // setting subscriber + // are NO_SUBSCRIBER_[NO_RESULT or HAS_RESULT] + if (state == NO_SUBSCRIBER_NO_RESULT) { + if (STATE.compareAndSet(this, NO_SUBSCRIBER_NO_RESULT, NO_REQUEST_NO_RESULT)) { + state = NO_REQUEST_NO_RESULT; + } else { + // the possible false position is racing with [onNext / onError / onComplete / dispose] + // which are going to put the state in the NO_REQUEST_HAS_RESULT + STATE.set(this, NO_REQUEST_HAS_RESULT); + state = NO_REQUEST_HAS_RESULT; + } + } else { + STATE.set(this, NO_REQUEST_HAS_RESULT); + state = NO_REQUEST_HAS_RESULT; + } + + // check if state is with a result then there is a chance of immediate termination if there is + // no value + // e.g. [onError / onComplete / dispose] only + if (state == NO_REQUEST_HAS_RESULT && this.value == null) { + this.actual = null; + Throwable e = this.error; + // barrier to flush changes + STATE.set(this, HAS_REQUEST_HAS_RESULT); + if (e == null) { + Operators.complete(actual); + } else { + Operators.error(actual, e); + } + return; + } + + // call onSubscribe if has value in the result or no result delivered so far + actual.onSubscribe(this); + } else { + Operators.error( + actual, + new IllegalStateException("UnicastMonoProcessor allows only a single Subscriber")); + } } @Override - public void onNext(O o) { - processor.onNext(o); + public final void request(long n) { + if (Operators.validate(n)) { + for (; ; ) { + int s = state; + // if the any bits 1-31 are set, we are either in fusion mode (FUSED_*) + // or request has been called (HAS_REQUEST_*) + if ((s & ~NO_REQUEST_HAS_RESULT) != 0) { + return; + } + if (s == NO_REQUEST_HAS_RESULT + && STATE.compareAndSet(this, NO_REQUEST_HAS_RESULT, HAS_REQUEST_HAS_RESULT)) { + O v = value; + if (v != null) { + value = null; + Subscriber a = actual; + a.onNext(v); + a.onComplete(); + } + return; + } + if (STATE.compareAndSet(this, NO_REQUEST_NO_RESULT, HAS_REQUEST_NO_RESULT)) { + return; + } + } + } } @Override - public void onError(Throwable t) { - processor.onError(t); - } + public final void cancel() { + if (STATE.getAndSet(this, CANCELLED) <= HAS_REQUEST_NO_RESULT) { + Operators.onDiscard(value, currentContext()); + } - @Nullable - public Throwable getError() { - return processor.getError(); - } + final Subscription s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription()); + if (s != null) { + s.cancel(); + } - public boolean isCancelled() { - return processor.isCancelled(); + value = null; + actual = null; } - public boolean isError() { - return processor.isError(); - } + @Override + public void dispose() { + final Subscription s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription()); + if (s == Operators.cancelledSubscription()) { + return; + } - public boolean isSuccess() { - return processor.isSuccess(); - } + if (s != null) { + s.cancel(); + } - public boolean isTerminated() { - return processor.isTerminated(); + complete(new CancellationException("Disposed")); } + /** + * Returns the value that completed this {@link UnicastMonoProcessor}. Returns {@code null} if the + * {@link UnicastMonoProcessor} has not been completed. If the {@link UnicastMonoProcessor} is + * completed with an error a RuntimeException that wraps the error is thrown. + * + * @return the value that completed the {@link UnicastMonoProcessor}, or {@code null} if it has + * not been completed + * @throws RuntimeException if the {@link UnicastMonoProcessor} was completed with an error + */ @Nullable public O peek() { - return processor.peek(); - } + if (isCancelled()) { + return null; + } + + if (value != null) { + return value; + } + + if (error != null) { + RuntimeException re = Exceptions.propagate(error); + re = Exceptions.addSuppressed(re, new Exception("Mono#peek terminated with an error")); + throw re; + } - public long downstreamCount() { - return processor.downstreamCount(); + return null; } - public boolean hasDownstreams() { - return processor.hasDownstreams(); + /** + * Set the value internally, without impacting request tracking state. + * + * @param value the new value. + * @see #complete(Object) + */ + private void setValue(O value) { + this.value = value; } - @Override - public void onComplete() { - processor.onComplete(); + /** + * Set the error internally, without impacting request tracking state. + * + * @param throwable the error. + * @see #complete(Object) + */ + private void setError(Throwable throwable) { + this.error = throwable; } - @Override - public void request(long n) { - processor.request(n); + /** + * Return the produced {@link Throwable} error if any or null + * + * @return the produced {@link Throwable} error if any or null + */ + @Nullable + public final Throwable getError() { + return isDisposed() ? error : null; } - @Override - public void cancel() { - processor.cancel(); + /** + * Indicates whether this {@code UnicastMonoProcessor} has been completed with an error. + * + * @return {@code true} if this {@code UnicastMonoProcessor} was completed with an error, {@code + * false} otherwise. + */ + public final boolean isError() { + return getError() != null; } - @Override - public void dispose() { - processor.dispose(); + /** + * Indicates whether this {@code UnicastMonoProcessor} has been interrupted via cancellation. + * + * @return {@code true} if this {@code UnicastMonoProcessor} is cancelled, {@code false} + * otherwise. + */ + public boolean isCancelled() { + return state == CANCELLED; } - @Override - public Context currentContext() { - return processor.currentContext(); + public final boolean isTerminated() { + int state = this.state; + return (state < CANCELLED && state % 2 == 1); } @Override public boolean isDisposed() { - return processor.isDisposed(); + int state = this.state; + return state == CANCELLED || (state < CANCELLED && state % 2 == 1); } @Override + @Nullable public Object scanUnsafe(Attr key) { - return processor.scanUnsafe(key); - } + // touch guard + int state = this.state; - @Override - public void subscribe(CoreSubscriber actual) { - Objects.requireNonNull(actual, "subscribe"); - if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { - processor.subscribe(actual); - } else { - Operators.error( - actual, - new IllegalStateException("UnicastMonoProcessor allows only a single Subscriber")); + if (key == Attr.TERMINATED) { + return (state < CANCELLED && state % 2 == 1); + } + if (key == Attr.PARENT) { + return subscription; } + if (key == Attr.ERROR) { + return error; + } + if (key == Attr.PREFETCH) { + return Integer.MAX_VALUE; + } + if (key == Attr.CANCELLED) { + return state == CANCELLED; + } + return null; + } + + /** + * Return true if any {@link Subscriber} is actively subscribed + * + * @return true if any {@link Subscriber} is actively subscribed + */ + public final boolean hasDownstream() { + return state > NO_SUBSCRIBER_HAS_RESULT && actual != null; } } diff --git a/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java b/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java new file mode 100644 index 000000000..9e4357bab --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java @@ -0,0 +1,1451 @@ +/* + * Copyright 2015-2018 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.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.rsocket.internal.subscriber.AssertSubscriber; +import java.lang.ref.WeakReference; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Date; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.Scannable; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Operators; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; +import reactor.test.util.RaceTestUtils; +import reactor.util.context.Context; +import reactor.util.function.Tuple2; + +public class UnicastMonoProcessorTest { + + @Test + public void testUnicast() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + assertThatThrownBy(() -> RaceTestUtils.race(processor::subscribe, processor::subscribe)) + .hasCause( + new IllegalStateException("UnicastMonoProcessor allows only a single Subscriber")); + } + } + + @Test + public void stateFlowTest1_Next() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.onNext(1); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoEvents(); + assertSubscriber.request(1); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertValues(1); + assertSubscriber.assertComplete(); + } + + @Test + public void stateFlowTest1_Complete() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.onComplete(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoValues(); + assertSubscriber.assertComplete(); + } + + @Test + public void stateFlowTest1_Error() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + RuntimeException testError = new RuntimeException("test"); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.onError(testError); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoValues(); + assertSubscriber.assertError(RuntimeException.class); + assertSubscriber.assertErrorMessage("test"); + } + + @Test + public void stateFlowTest1_Dispose() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.dispose(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoValues(); + assertSubscriber.assertError(CancellationException.class); + assertSubscriber.assertErrorMessage("Disposed"); + } + + @Test + public void stateFlowTest2_Next() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + processor.onNext(1); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoEvents(); + assertSubscriber.request(1); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertValues(1); + assertSubscriber.assertComplete(); + } + + @Test + public void stateFlowTest2_Complete() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + processor.onComplete(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoValues(); + assertSubscriber.assertComplete(); + } + + @Test + public void stateFlowTest2_Error() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + processor.onError(new RuntimeException("Test")); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoValues(); + assertSubscriber.assertError(RuntimeException.class); + assertSubscriber.assertErrorMessage("Test"); + } + + @Test + public void stateFlowTest2_Dispose() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + processor.dispose(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoValues(); + assertSubscriber.assertError(CancellationException.class); + assertSubscriber.assertErrorMessage("Disposed"); + } + + @Test + public void stateFlowTest3_Next() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + assertSubscriber.assertNoEvents(); + assertSubscriber.request(1); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); + + assertSubscriber.assertNoEvents(); + processor.onNext(1); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertValues(1); + assertSubscriber.assertComplete(); + } + + @Test + public void stateFlowTest3_Complete() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + assertSubscriber.request(1); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); + + processor.onComplete(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoValues(); + assertSubscriber.assertComplete(); + } + + @Test + public void stateFlowTest3_Error() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + assertSubscriber.request(1); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); + + processor.onError(new RuntimeException("Test")); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoValues(); + assertSubscriber.assertError(RuntimeException.class); + assertSubscriber.assertErrorMessage("Test"); + } + + @Test + public void stateFlowTest3_Dispose() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + assertSubscriber.request(1); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); + + processor.dispose(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoValues(); + assertSubscriber.assertError(RuntimeException.class); + assertSubscriber.assertErrorMessage("Disposed"); + } + + @Test + public void stateFlowTest4_Next() { + ArrayList discarded = new ArrayList<>(); + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + // Context discardingContext = Operators.enableOnDiscard(null, discarded::add); + Hooks.onNextDropped(discarded::add); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + + try { + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + assertSubscriber.request(1); + assertSubscriber.assertNoEvents(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); + + assertSubscriber.cancel(); + assertSubscriber.assertNoEvents(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + processor.onNext(1); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + assertSubscriber.assertNoEvents(); + assertThat(discarded).containsExactly(1); + } finally { + Hooks.resetOnNextDropped(); + } + } + + @Test + public void stateFlowTest4_Error() { + ArrayList discarded = new ArrayList<>(); + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + // Context discardingContext = Operators.enableOnDiscard(null, discarded::add); + Hooks.onErrorDropped(discarded::add); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + + try { + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + assertSubscriber.request(1); + assertSubscriber.assertNoEvents(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); + + assertSubscriber.cancel(); + assertSubscriber.assertNoEvents(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + RuntimeException testError = new RuntimeException("test"); + processor.onError(testError); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + assertSubscriber.assertNoEvents(); + assertThat(discarded).containsExactly(testError); + + } finally { + Hooks.resetOnErrorDropped(); + } + } + + @Test + public void stateFlowTest4_Dispose() { + ArrayList discarded = new ArrayList<>(); + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + // Context discardingContext = Operators.enableOnDiscard(null, discarded::add); + Hooks.onErrorDropped(discarded::add); + try { + AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + assertSubscriber.request(1); + assertSubscriber.assertNoEvents(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); + + assertSubscriber.cancel(); + assertSubscriber.assertNoEvents(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + processor.dispose(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + assertSubscriber.assertNoEvents(); + assertThat(discarded).isEmpty(); + } finally { + Hooks.resetOnErrorDropped(); + } + } + + @Test + public void stateFlowTest4_Complete() { + ArrayList discarded = new ArrayList<>(); + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + // Context discardingContext = Operators.enableOnDiscard(null, discarded::add); + Hooks.onErrorDropped(discarded::add); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + + try { + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + assertSubscriber.request(1); + assertSubscriber.assertNoEvents(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); + + assertSubscriber.cancel(); + assertSubscriber.assertNoEvents(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + processor.onComplete(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + assertSubscriber.assertNoEvents(); + assertThat(discarded).isEmpty(); + } finally { + Hooks.resetOnErrorDropped(); + } + } + + @Test + public void stateFlowTest5_Next() { + ArrayList discarded = new ArrayList<>(); + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + Context discardingContext = Operators.enableOnDiscard(null, discarded::add); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + processor.onNext(1); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_HAS_RESULT); + + assertSubscriber.cancel(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + assertSubscriber.assertNoEvents(); + assertThat(discarded).containsExactly(1); + } + + @Test + public void stateFlowTest5_Complete() { + ArrayList discarded = new ArrayList<>(); + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + Context discardingContext = Operators.enableOnDiscard(null, discarded::add); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + processor.onComplete(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.cancel(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + assertSubscriber.assertComplete(); + assertThat(discarded).isEmpty(); + } + + @Test + public void stateFlowTest5_Error() { + ArrayList discarded = new ArrayList<>(); + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + Context discardingContext = Operators.enableOnDiscard(null, discarded::add); + Hooks.onErrorDropped(discarded::add); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); + + try { + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + processor.onError(new RuntimeException("test")); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.cancel(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + assertSubscriber.assertError(RuntimeException.class); + assertSubscriber.assertErrorMessage("test"); + assertThat(discarded).isEmpty(); + } finally { + Hooks.resetOnErrorDropped(); + } + } + + @Test + public void stateFlowTest5_Dispose() { + ArrayList discarded = new ArrayList<>(); + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + Context discardingContext = Operators.enableOnDiscard(null, discarded::add); + Hooks.onErrorDropped(discarded::add); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); + + try { + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); + + processor.dispose(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.cancel(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + assertSubscriber.assertError(CancellationException.class); + assertSubscriber.assertErrorMessage("Disposed"); + assertThat(discarded).isEmpty(); + } finally { + Hooks.resetOnErrorDropped(); + } + } + + @Test + public void stateFlowTest6_Next() { + ArrayList discarded = new ArrayList<>(); + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + Context discardingContext = Operators.enableOnDiscard(null, discarded::add); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.onNext(1); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_HAS_RESULT); + + assertSubscriber.cancel(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + assertSubscriber.assertNoEvents(); + assertThat(discarded).containsExactly(1); + } + + @Test + public void stateFlowTest7_Next() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + RaceTestUtils.race( + () -> processor.onNext(1), + () -> processor.subscribe(assertSubscriber), + Schedulers.single()); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertValues(1); + assertSubscriber.assertComplete(); + } + } + + @Test + public void stateFlowTest7_Complete() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + RaceTestUtils.race( + processor::onComplete, () -> processor.subscribe(assertSubscriber), Schedulers.single()); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoValues(); + assertSubscriber.assertComplete(); + } + } + + @Test + public void stateFlowTest7_Error() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + RaceTestUtils.race( + () -> processor.onError(new RuntimeException("test")), + () -> processor.subscribe(assertSubscriber), + Schedulers.single()); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoValues(); + assertSubscriber.assertError(RuntimeException.class); + assertSubscriber.assertErrorMessage("test"); + } + } + + @Test + public void stateFlowTest7_Dispose() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + RaceTestUtils.race( + processor::dispose, () -> processor.subscribe(assertSubscriber), Schedulers.single()); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertNoValues(); + assertSubscriber.assertError(CancellationException.class); + assertSubscriber.assertErrorMessage("Disposed"); + } + } + + @Test + public void stateFlowTest8_Next() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); + + RaceTestUtils.race(() -> processor.onNext(1), assertSubscriber::cancel, Schedulers.single()); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + if (assertSubscriber.values().isEmpty()) { + assertSubscriber.assertNoEvents(); + } else { + assertSubscriber.assertValues(1); + assertSubscriber.assertComplete(); + } + } + } + + @Test + public void stateFlowTest9_Next() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); + + RaceTestUtils.race(() -> processor.onNext(1), processor::dispose, Schedulers.single()); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + if (processor.isError()) { + assertSubscriber.assertNoValues(); + assertSubscriber.assertErrorMessage("Disposed"); + } else { + assertSubscriber.assertValues(1); + assertSubscriber.assertComplete(); + } + } + } + + @Test + public void stateFlowTest13_Next() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.onNext(1); + processor.subscribe(assertSubscriber); + + RaceTestUtils.race( + () -> assertSubscriber.request(1), + () -> assertSubscriber.request(1), + Schedulers.single()); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertValues(1); + assertSubscriber.assertComplete(); + } + } + + @Test + public void stateFlowTest14_Next() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + assertSubscriber.request(1); + + RaceTestUtils.race(() -> processor.onNext(1), () -> processor.onNext(1), Schedulers.single()); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); + + assertSubscriber.assertValues(1); + assertSubscriber.assertComplete(); + } + } + + @Test + public void noRetentionOnTermination() throws InterruptedException { + Date date = new Date(); + CompletableFuture future = new CompletableFuture<>(); + + WeakReference refDate = new WeakReference<>(date); + WeakReference> refFuture = new WeakReference<>(future); + + Mono source = Mono.fromFuture(future); + Mono data = + source.map(Date::toString).log().subscribeWith(UnicastMonoProcessor.create()).log(); + + future.complete(date); + assertThat(data.block()).isEqualTo(date.toString()); + + date = null; + future = null; + source = null; + System.gc(); + + int cycles; + for (cycles = 10; cycles > 0; cycles--) { + if (refDate.get() == null && refFuture.get() == null) break; + Thread.sleep(100); + } + + assertThat(refFuture.get()).isNull(); + assertThat(refDate.get()).isNull(); + assertThat(cycles).isNotZero().isPositive(); + } + + @Test + public void noRetentionOnTerminationError() throws InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + + WeakReference> refFuture = new WeakReference<>(future); + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + + Mono source = Mono.fromFuture(future); + Mono data = source.map(Date::toString).subscribeWith(processor); + + future.completeExceptionally(new IllegalStateException()); + + assertThatExceptionOfType(IllegalStateException.class).isThrownBy(data::block); + + future = null; + source = null; + System.gc(); + + int cycles; + for (cycles = 10; cycles > 0; cycles--) { + if (refFuture.get() == null) break; + Thread.sleep(100); + } + + assertThat(refFuture.get()).isNull(); + assertThat(cycles).isNotZero().isPositive(); + } + + @Test + public void noRetentionOnTerminationCancel() throws InterruptedException { + CompletableFuture future = new CompletableFuture<>(); + + WeakReference> refFuture = new WeakReference<>(future); + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + + Mono source = Mono.fromFuture(future); + Mono data = + source.map(Date::toString).transformDeferred((s) -> s.subscribeWith(processor)); + + future = null; + source = null; + + data.subscribe().dispose(); + processor.dispose(); + + data = null; + + System.gc(); + + int cycles; + for (cycles = 10; cycles > 0; cycles--) { + if (refFuture.get() == null) break; + Thread.sleep(100); + } + + assertThat(refFuture.get()).isNull(); + assertThat(cycles).isNotZero().isPositive(); + } + + @Test(expected = IllegalStateException.class) + public void MonoProcessorResultNotAvailable() { + MonoProcessor mp = MonoProcessor.create(); + mp.block(Duration.ofMillis(1)); + } + + @Test + public void MonoProcessorRejectedDoOnSuccessOrError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + mp.doOnSuccessOrError((s, f) -> ref.set(f)).subscribe(); + mp.onError(new Exception("test")); + + assertThat(ref.get()).hasMessage("test"); + assertThat(mp.isError()).isTrue(); + } + + @Test + public void MonoProcessorRejectedDoOnTerminate() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicInteger invoked = new AtomicInteger(); + + mp.doOnTerminate(invoked::incrementAndGet).subscribe(); + mp.onError(new Exception("test")); + + assertThat(invoked.get()).isEqualTo(1); + assertThat(mp.isError()).isTrue(); + } + + @Test + public void MonoProcessorRejectedSubscribeCallback() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + mp.subscribe(v -> {}, ref::set); + mp.onError(new Exception("test")); + + assertThat(ref.get()).hasMessage("test"); + assertThat(mp.isError()).isTrue(); + } + + @Test + public void MonoProcessorSuccessDoOnSuccessOrError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + mp.doOnSuccessOrError((s, f) -> ref.set(s)).subscribe(); + mp.onNext("test"); + + assertThat(ref.get()).isEqualToIgnoringCase("test"); + assertThat(mp.isDisposed()).isTrue(); + assertThat(mp.isError()).isFalse(); + } + + @Test + public void MonoProcessorSuccessDoOnTerminate() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicInteger invoked = new AtomicInteger(); + + mp.doOnTerminate(invoked::incrementAndGet).subscribe(); + mp.onNext("test"); + + assertThat(invoked.get()).isEqualTo(1); + assertThat(mp.isDisposed()).isTrue(); + assertThat(mp.isError()).isFalse(); + } + + @Test + public void MonoProcessorSuccessSubscribeCallback() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + mp.subscribe(ref::set); + mp.onNext("test"); + + assertThat(ref.get()).isEqualToIgnoringCase("test"); + assertThat(mp.isDisposed()).isTrue(); + assertThat(mp.isError()).isFalse(); + } + + @Test + public void MonoProcessorRejectedDoOnError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + mp.doOnError(ref::set).subscribe(); + mp.onError(new Exception("test")); + + assertThat(ref.get()).hasMessage("test"); + assertThat(mp.isError()).isTrue(); + } + + @Test(expected = NullPointerException.class) + public void MonoProcessorRejectedSubscribeCallbackNull() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.subscribe((Subscriber) null); + } + + @Test + public void MonoProcessorSuccessDoOnSuccess() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + mp.doOnSuccess(ref::set).subscribe(); + mp.onNext("test"); + + assertThat(ref.get()).isEqualToIgnoringCase("test"); + assertThat(mp.isDisposed()).isTrue(); + assertThat(mp.isError()).isFalse(); + } + + @Test + public void MonoProcessorSuccessChainTogether() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + mp.subscribe(mp2); + + mp.onNext("test"); + + assertThat(mp2.peek()).isEqualToIgnoringCase("test"); + assertThat(mp.isDisposed()).isTrue(); + assertThat(mp.isError()).isFalse(); + } + + @Test + public void MonoProcessorRejectedChainTogether() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + mp.subscribe(mp2); + + mp.onError(new Exception("test")); + + assertThat(mp2.getError()).hasMessage("test"); + assertThat(mp.isError()).isTrue(); + } + + @Test + public void MonoProcessorDoubleFulfill() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + StepVerifier.create(mp) + .then( + () -> { + mp.onNext("test1"); + mp.onNext("test2"); + }) + .expectNext("test1") + .expectComplete() + .verifyThenAssertThat() + .hasDroppedExactly("test2"); + } + + @Test + public void MonoProcessorNullFulfill() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.onNext(null); + + assertThat(mp.isDisposed()).isTrue(); + assertThat(mp.peek()).isNull(); + } + + @Test + public void MonoProcessorMapFulfill() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.onNext(1); + + UnicastMonoProcessor mp2 = + mp.map(s -> s * 2).subscribeWith(UnicastMonoProcessor.create()); + + assertThat(mp2.isDisposed()).isTrue(); + assertThat(mp2.isTerminated()).isTrue(); + assertThat(mp2.isCancelled()).isFalse(); + assertThat(mp2.peek()).isEqualTo(2); + + mp2.subscribe(); + } + + @Test + public void MonoProcessorThenFulfill() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.onNext(1); + + UnicastMonoProcessor mp2 = + mp.flatMap(s -> Mono.just(s * 2)).subscribeWith(UnicastMonoProcessor.create()); + + assertThat(mp2.isDisposed()).isTrue(); + assertThat(mp2.isTerminated()).isTrue(); + assertThat(mp2.isCancelled()).isFalse(); + assertThat(mp2.peek()).isEqualTo(2); + + mp2.subscribe(); + } + + @Test + public void MonoProcessorMapError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.onNext(1); + + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + + StepVerifier.create( + mp.map( + s -> { + throw new RuntimeException("test"); + }) + .subscribeWith(mp2), + 0) + .thenRequest(1) + .then( + () -> { + assertThat(mp2.isDisposed()).isTrue(); + assertThat(mp2.getError()).hasMessage("test"); + }) + .verifyErrorMessage("test"); + } + + @Test(expected = Exception.class) + public void MonoProcessorDoubleError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.onError(new Exception("test")); + mp.onError(new Exception("test")); + } + + @Test(expected = Exception.class) + public void MonoProcessorDoubleSignal() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + + mp.onNext("test"); + mp.onError(new Exception("test")); + } + + @Test + public void zipMonoProcessor() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + UnicastMonoProcessor> mp3 = UnicastMonoProcessor.create(); + + StepVerifier.create(Mono.zip(mp, mp2).subscribeWith(mp3), 0) + .then(() -> assertThat(mp3.isDisposed()).isFalse()) + .then(() -> mp.onNext(1)) + .then(() -> assertThat(mp3.isDisposed()).isFalse()) + .then(() -> mp2.onNext(2)) + .then( + () -> { + assertThat(mp3.isDisposed()).isTrue(); + assertThat(mp3.peek().getT1()).isEqualTo(1); + assertThat(mp3.peek().getT2()).isEqualTo(2); + }) + .thenRequest(1) + .expectNextMatches(t -> t.getT1() == 1 && t.getT2() == 2) + .verifyComplete(); + } + + @Test + public void zipMonoProcessor2() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp3 = UnicastMonoProcessor.create(); + + StepVerifier.create(Mono.zip(d -> (Integer) d[0], mp).subscribeWith(mp3), 0) + .then(() -> assertThat(mp3.isDisposed()).isFalse()) + .then(() -> mp.onNext(1)) + .then( + () -> { + assertThat(mp3.isDisposed()).isTrue(); + assertThat(mp3.peek()).isEqualTo(1); + }) + .thenRequest(1) + .expectNext(1) + .verifyComplete(); + } + + @Test + public void zipMonoProcessorRejected() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + UnicastMonoProcessor> mp3 = UnicastMonoProcessor.create(); + + StepVerifier.create(Mono.zip(mp, mp2).subscribeWith(mp3)) + .then(() -> assertThat(mp3.isDisposed()).isFalse()) + .then(() -> mp.onError(new Exception("test"))) + .then( + () -> { + assertThat(mp3.isDisposed()).isTrue(); + assertThat(mp3.getError()).hasMessage("test"); + }) + .verifyErrorMessage("test"); + } + + @Test + public void filterMonoProcessor() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + StepVerifier.create(mp.filter(s -> s % 2 == 0).subscribeWith(mp2), 0) + .then(() -> mp.onNext(2)) + .then(() -> assertThat(mp2.isError()).isFalse()) + .then(() -> assertThat(mp2.isDisposed()).isTrue()) + .then(() -> assertThat(mp2.peek()).isEqualTo(2)) + .then(() -> assertThat(mp2.isDisposed()).isTrue()) + .thenRequest(1) + .expectNext(2) + .verifyComplete(); + } + + @Test + public void filterMonoProcessorNot() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + StepVerifier.create(mp.filter(s -> s % 2 == 0).subscribeWith(mp2)) + .then(() -> mp.onNext(1)) + .then(() -> assertThat(mp2.isError()).isFalse()) + .then(() -> assertThat(mp2.isDisposed()).isTrue()) + .then(() -> assertThat(mp2.peek()).isNull()) + .then(() -> assertThat(mp2.isDisposed()).isTrue()) + .verifyComplete(); + } + + @Test + public void filterMonoProcessorError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + StepVerifier.create( + mp.filter( + s -> { + throw new RuntimeException("test"); + }) + .subscribeWith(mp2)) + .then(() -> mp.onNext(2)) + .then(() -> assertThat(mp2.isError()).isTrue()) + .then(() -> assertThat(mp2.isDisposed()).isTrue()) + .then(() -> assertThat(mp2.getError()).hasMessage("test")) + .then(() -> assertThat(mp2.isDisposed()).isTrue()) + .verifyErrorMessage("test"); + } + + @Test + public void doOnSuccessMonoProcessorError() { + UnicastMonoProcessor mp = UnicastMonoProcessor.create(); + UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); + AtomicReference ref = new AtomicReference<>(); + + StepVerifier.create( + mp.doOnSuccess( + s -> { + throw new RuntimeException("test"); + }) + .doOnError(ref::set) + .subscribeWith(mp2)) + .then(() -> mp.onNext(2)) + .then(() -> assertThat(mp2.isError()).isTrue()) + .then(() -> assertThat(ref.get()).hasMessage("test")) + .then(() -> assertThat(mp2.isDisposed()).isTrue()) + .then(() -> assertThat(mp2.getError()).hasMessage("test")) + .then(() -> assertThat(mp2.isDisposed()).isTrue()) + .verifyErrorMessage("test"); + } + + @Test + public void fluxCancelledByMonoProcessor() { + AtomicLong cancelCounter = new AtomicLong(); + Flux.range(1, 10) + .doOnCancel(cancelCounter::incrementAndGet) + .transformDeferred((s) -> s.subscribeWith(UnicastMonoProcessor.create())) + .subscribe(); + + assertThat(cancelCounter.get()).isEqualTo(1); + } + + @Test + public void cancelledByMonoProcessor() { + AtomicLong cancelCounter = new AtomicLong(); + UnicastMonoProcessor monoProcessor = + Mono.just("foo") + .doOnCancel(cancelCounter::incrementAndGet) + .subscribeWith(UnicastMonoProcessor.create()); + monoProcessor.subscribe(); + + assertThat(cancelCounter.get()).isEqualTo(1); + } + + @Test + public void scanProcessor() { + UnicastMonoProcessor test = UnicastMonoProcessor.create(); + Subscription subscription = Operators.emptySubscription(); + test.onSubscribe(subscription); + + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); + assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); + + test.onComplete(); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); + assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); + } + + @Test + public void scanProcessorCancelled() { + UnicastMonoProcessor test = UnicastMonoProcessor.create(); + Subscription subscription = Operators.emptySubscription(); + test.onSubscribe(subscription); + + assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); + assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); + + test.cancel(); + assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); + assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); + } + + @Test + public void scanProcessorSubscription() { + UnicastMonoProcessor test = UnicastMonoProcessor.create(); + Subscription subscription = Operators.emptySubscription(); + test.onSubscribe(subscription); + + assertThat(test.scan(Scannable.Attr.ACTUAL)).isNull(); + assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(subscription); + } + + @Test + public void scanProcessorError() { + UnicastMonoProcessor test = UnicastMonoProcessor.create(); + Subscription subscription = Operators.emptySubscription(); + test.onSubscribe(subscription); + + test.onError(new IllegalStateException("boom")); + + assertThat(test.scan(Scannable.Attr.ERROR)).hasMessage("boom"); + } + + @Test + public void monoToProcessorConnects() { + TestPublisher tp = TestPublisher.create(); + UnicastMonoProcessor connectedProcessor = + tp.mono().subscribeWith(UnicastMonoProcessor.create()); + + assertThat(connectedProcessor.subscription).isNotNull(); + } + + @Test + public void monoToProcessorChain() { + StepVerifier.withVirtualTime( + () -> + Mono.just("foo") + .subscribeWith(UnicastMonoProcessor.create()) + .delayElement(Duration.ofMillis(500))) + .expectSubscription() + .expectNoEvent(Duration.ofMillis(500)) + .expectNext("foo") + .verifyComplete(); + } + + @Test + public void monoToProcessorChainColdToHot() { + AtomicInteger subscriptionCount = new AtomicInteger(); + Mono coldToHot = + Mono.just("foo") + .doOnSubscribe(sub -> subscriptionCount.incrementAndGet()) + .transformDeferred(s -> s.subscribeWith(UnicastMonoProcessor.create())) + .subscribeWith(UnicastMonoProcessor.create()) // this actually subscribes + .filter(s -> s.length() < 4); + + assertThat(subscriptionCount.get()).isEqualTo(1); + + coldToHot.block(); + assertThatThrownBy(coldToHot::block) + .hasMessage("UnicastMonoProcessor allows only a single Subscriber"); + assertThatThrownBy(coldToHot::block) + .hasMessage("UnicastMonoProcessor allows only a single Subscriber"); + + assertThat(subscriptionCount.get()).isEqualTo(1); + } + + @Test + public void monoProcessorBlockIsUnbounded() { + long start = System.nanoTime(); + + String result = + Mono.just("foo") + .delayElement(Duration.ofMillis(500)) + .subscribeWith(UnicastMonoProcessor.create()) + .block(); + + assertThat(result).isEqualTo("foo"); + assertThat(Duration.ofNanos(System.nanoTime() - start)) + .isGreaterThanOrEqualTo(Duration.ofMillis(500)); + } + + @Test + public void monoProcessorBlockNegativeIsImmediateTimeout() { + long start = System.nanoTime(); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy( + () -> + Mono.just("foo") + .delayElement(Duration.ofMillis(500)) + .subscribeWith(UnicastMonoProcessor.create()) + .block(Duration.ofSeconds(-1))) + .withMessage("Timeout on blocking read for -1000 MILLISECONDS"); + + assertThat(Duration.ofNanos(System.nanoTime() - start)).isLessThan(Duration.ofMillis(500)); + } + + @Test + public void monoProcessorBlockZeroIsImmediateTimeout() { + long start = System.nanoTime(); + + assertThatExceptionOfType(IllegalStateException.class) + .isThrownBy( + () -> + Mono.just("foo") + .delayElement(Duration.ofMillis(500)) + .subscribeWith(UnicastMonoProcessor.create()) + .block(Duration.ZERO)) + .withMessage("Timeout on blocking read for 0 MILLISECONDS"); + + assertThat(Duration.ofNanos(System.nanoTime() - start)).isLessThan(Duration.ofMillis(500)); + } + + @Test + public void disposeBeforeValueSendsCancellationException() { + UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + AtomicReference e1 = new AtomicReference<>(); + AtomicReference e2 = new AtomicReference<>(); + AtomicReference e3 = new AtomicReference<>(); + AtomicReference late = new AtomicReference<>(); + + processor.subscribe(v -> Assertions.fail("expected first subscriber to error"), e1::set); + processor.subscribe(v -> Assertions.fail("expected second subscriber to error"), e2::set); + processor.subscribe(v -> Assertions.fail("expected third subscriber to error"), e3::set); + + processor.dispose(); + + assertThat(e1.get()).isInstanceOf(CancellationException.class); + assertThat(e2.get()).isInstanceOf(IllegalStateException.class); + assertThat(e3.get()).isInstanceOf(IllegalStateException.class); + + processor.subscribe(v -> Assertions.fail("expected late subscriber to error"), late::set); + assertThat(late.get()).isInstanceOf(IllegalStateException.class); + } + + static void warmup(Scheduler scheduler) throws InterruptedException { + scheduler.start(); + + // warm up + CountDownLatch latch = new CountDownLatch(10000); + for (int i = 0; i < 10000; i++) { + scheduler.schedule(latch::countDown); + } + latch.await(5, TimeUnit.SECONDS); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/internal/subscriber/AssertSubscriber.java b/rsocket-core/src/test/java/io/rsocket/internal/subscriber/AssertSubscriber.java new file mode 100644 index 000000000..84a589a8d --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/internal/subscriber/AssertSubscriber.java @@ -0,0 +1,1154 @@ +/* + * Copyright (c) 2011-2017 Pivotal Software Inc, All Rights Reserved. + * + * 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 + * + * https://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.internal.subscriber; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.core.publisher.Operators; +import reactor.util.annotation.NonNull; +import reactor.util.context.Context; + +/** + * A Subscriber implementation that hosts assertion tests for its state and allows asynchronous + * cancellation and requesting. + * + *

To create a new instance of {@link AssertSubscriber}, you have the choice between these static + * methods: + * + *

    + *
  • {@link AssertSubscriber#create()}: create a new {@link AssertSubscriber} and requests an + * unbounded number of elements. + *
  • {@link AssertSubscriber#create(long)}: create a new {@link AssertSubscriber} and requests + * {@code n} elements (can be 0 if you want no initial demand). + *
+ * + *

If you are testing asynchronous publishers, don't forget to use one of the {@code await*()} + * methods to wait for the data to assert. + * + *

You can extend this class but only the onNext, onError and onComplete can be overridden. You + * can call {@link #request(long)} and {@link #cancel()} from any thread or from within the + * overridable methods but you should avoid calling the assertXXX methods asynchronously. + * + *

Usage: + * + *

{@code
+ * AssertSubscriber
+ *   .subscribe(publisher)
+ *   .await()
+ *   .assertValues("ABC", "DEF");
+ * }
+ * + * @param the value type. + * @author Sebastien Deleuze + * @author David Karnok + * @author Anatoly Kadyshev + * @author Stephane Maldini + * @author Brian Clozel + */ +public class AssertSubscriber implements CoreSubscriber, Subscription { + + /** Default timeout for waiting next values to be received */ + public static final Duration DEFAULT_VALUES_TIMEOUT = Duration.ofSeconds(3); + + @SuppressWarnings("rawtypes") + private static final AtomicLongFieldUpdater REQUESTED = + AtomicLongFieldUpdater.newUpdater(AssertSubscriber.class, "requested"); + + @SuppressWarnings("rawtypes") + private static final AtomicReferenceFieldUpdater NEXT_VALUES = + AtomicReferenceFieldUpdater.newUpdater(AssertSubscriber.class, List.class, "values"); + + @SuppressWarnings("rawtypes") + private static final AtomicReferenceFieldUpdater S = + AtomicReferenceFieldUpdater.newUpdater(AssertSubscriber.class, Subscription.class, "s"); + + private final Context context; + + private final List errors = new LinkedList<>(); + + private final CountDownLatch cdl = new CountDownLatch(1); + + volatile Subscription s; + + volatile long requested; + + volatile List values = new LinkedList<>(); + + /** The fusion mode to request. */ + private int requestedFusionMode = -1; + + /** The established fusion mode. */ + private volatile int establishedFusionMode = -1; + + /** The fuseable QueueSubscription in case a fusion mode was specified. */ + private Fuseable.QueueSubscription qs; + + private int subscriptionCount = 0; + + private int completionCount = 0; + + private volatile long valueCount = 0L; + + private volatile long nextValueAssertedCount = 0L; + + private Duration valuesTimeout = DEFAULT_VALUES_TIMEOUT; + + private boolean valuesStorage = true; + + // + // ============================================================================================================== + // Static methods + // + // ============================================================================================================== + + /** + * Blocking method that waits until {@code conditionSupplier} returns true, or if it does not + * before the specified timeout, throws an {@link AssertionError} with the specified error message + * supplier. + * + * @param timeout the timeout duration + * @param errorMessageSupplier the error message supplier + * @param conditionSupplier condition to break out of the wait loop + * @throws AssertionError + */ + public static void await( + Duration timeout, Supplier errorMessageSupplier, BooleanSupplier conditionSupplier) { + + Objects.requireNonNull(errorMessageSupplier); + Objects.requireNonNull(conditionSupplier); + Objects.requireNonNull(timeout); + + long timeoutNs = timeout.toNanos(); + long startTime = System.nanoTime(); + do { + if (conditionSupplier.getAsBoolean()) { + return; + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } while (System.nanoTime() - startTime < timeoutNs); + throw new AssertionError(errorMessageSupplier.get()); + } + + /** + * Blocking method that waits until {@code conditionSupplier} returns true, or if it does not + * before the specified timeout, throw an {@link AssertionError} with the specified error message. + * + * @param timeout the timeout duration + * @param errorMessage the error message + * @param conditionSupplier condition to break out of the wait loop + * @throws AssertionError + */ + public static void await( + Duration timeout, final String errorMessage, BooleanSupplier conditionSupplier) { + await( + timeout, + new Supplier() { + @Override + public String get() { + return errorMessage; + } + }, + conditionSupplier); + } + + /** + * Create a new {@link AssertSubscriber} that requests an unbounded number of elements. + * + *

Be sure at least a publisher has subscribed to it via {@link + * Publisher#subscribe(Subscriber)} before use assert methods. + * + * @param the observed value type + * @return a fresh AssertSubscriber instance + */ + public static AssertSubscriber create() { + return new AssertSubscriber<>(); + } + + /** + * Create a new {@link AssertSubscriber} that requests initially {@code n} elements. You can then + * manage the demand with {@link Subscription#request(long)}. + * + *

Be sure at least a publisher has subscribed to it via {@link + * Publisher#subscribe(Subscriber)} before use assert methods. + * + * @param n Number of elements to request (can be 0 if you want no initial demand). + * @param the observed value type + * @return a fresh AssertSubscriber instance + */ + public static AssertSubscriber create(long n) { + return new AssertSubscriber<>(n); + } + + // + // ============================================================================================================== + // constructors + // + // ============================================================================================================== + + public AssertSubscriber() { + this(Context.empty(), Long.MAX_VALUE); + } + + public AssertSubscriber(long n) { + this(Context.empty(), n); + } + + public AssertSubscriber(Context context) { + this(context, Long.MAX_VALUE); + } + + public AssertSubscriber(Context context, long n) { + if (n < 0) { + throw new IllegalArgumentException("initialRequest >= required but it was " + n); + } + this.context = context; + REQUESTED.lazySet(this, n); + } + + // + // ============================================================================================================== + // Configuration + // + // ============================================================================================================== + + /** + * Enable or disabled the values storage. It is enabled by default, and can be disable in order to + * be able to perform performance benchmarks or tests with a huge amount values. + * + * @param enabled enable value storage? + * @return this + */ + public final AssertSubscriber configureValuesStorage(boolean enabled) { + this.valuesStorage = enabled; + return this; + } + + /** + * Configure the timeout in seconds for waiting next values to be received (3 seconds by default). + * + * @param timeout the new default value timeout duration + * @return this + */ + public final AssertSubscriber configureValuesTimeout(Duration timeout) { + this.valuesTimeout = timeout; + return this; + } + + /** + * Returns the established fusion mode or -1 if it was not enabled + * + * @return the fusion mode, see Fuseable constants + */ + public final int establishedFusionMode() { + return establishedFusionMode; + } + + // + // ============================================================================================================== + // Assertions + // + // ============================================================================================================== + + /** + * Assert a complete successfully signal has been received. + * + * @return this + */ + public final AssertSubscriber assertComplete() { + assertNoError(); + int c = completionCount; + if (c == 0) { + throw new AssertionError("Not completed", null); + } + if (c > 1) { + throw new AssertionError("Multiple completions: " + c, null); + } + return this; + } + + /** + * Assert the specified values have been received. Values storage should be enabled to use this + * method. + * + * @param expectedValues the values to assert + * @see #configureValuesStorage(boolean) + * @return this + */ + public final AssertSubscriber assertContainValues(Set expectedValues) { + if (!valuesStorage) { + throw new IllegalStateException("Using assertNoValues() requires enabling values storage"); + } + if (expectedValues.size() > values.size()) { + throw new AssertionError("Actual contains fewer elements" + values, null); + } + + Iterator expected = expectedValues.iterator(); + + for (; ; ) { + boolean n2 = expected.hasNext(); + if (n2) { + T t2 = expected.next(); + if (!values.contains(t2)) { + throw new AssertionError( + "The element is not contained in the " + + "received results" + + " = " + + valueAndClass(t2), + null); + } + } else { + break; + } + } + return this; + } + + /** + * Assert an error signal has been received. + * + * @return this + */ + public final AssertSubscriber assertError() { + assertNotComplete(); + int s = errors.size(); + if (s == 0) { + throw new AssertionError("No error", null); + } + if (s > 1) { + throw new AssertionError("Multiple errors: " + s, null); + } + return this; + } + + /** + * Assert an error signal has been received. + * + * @param clazz The class of the exception contained in the error signal + * @return this + */ + public final AssertSubscriber assertError(Class clazz) { + assertNotComplete(); + int s = errors.size(); + if (s == 0) { + throw new AssertionError("No error", null); + } + if (s == 1) { + Throwable e = errors.get(0); + if (!clazz.isInstance(e)) { + throw new AssertionError( + "Error class incompatible: expected = " + clazz + ", actual = " + e, null); + } + } + if (s > 1) { + throw new AssertionError("Multiple errors: " + s, null); + } + return this; + } + + public final AssertSubscriber assertErrorMessage(String message) { + assertNotComplete(); + int s = errors.size(); + if (s == 0) { + assertionError("No error", null); + } + if (s == 1) { + if (!Objects.equals(message, errors.get(0).getMessage())) { + assertionError( + "Error class incompatible: expected = \"" + + message + + "\", actual = \"" + + errors.get(0).getMessage() + + "\"", + null); + } + } + if (s > 1) { + assertionError("Multiple errors: " + s, null); + } + + return this; + } + + /** + * Assert an error signal has been received. + * + * @param expectation A method that can verify the exception contained in the error signal and + * throw an exception (like an {@link AssertionError}) if the exception is not valid. + * @return this + */ + public final AssertSubscriber assertErrorWith(Consumer expectation) { + assertNotComplete(); + int s = errors.size(); + if (s == 0) { + throw new AssertionError("No error", null); + } + if (s == 1) { + expectation.accept(errors.get(0)); + } + if (s > 1) { + throw new AssertionError("Multiple errors: " + s, null); + } + return this; + } + + /** + * Assert that the upstream was a Fuseable source. + * + * @return this + */ + public final AssertSubscriber assertFuseableSource() { + if (qs == null) { + throw new AssertionError("Upstream was not Fuseable"); + } + return this; + } + + /** + * Assert that the fusion mode was granted. + * + * @return this + */ + public final AssertSubscriber assertFusionEnabled() { + if (establishedFusionMode != Fuseable.SYNC && establishedFusionMode != Fuseable.ASYNC) { + throw new AssertionError("Fusion was not enabled"); + } + return this; + } + + public final AssertSubscriber assertFusionMode(int expectedMode) { + if (establishedFusionMode != expectedMode) { + throw new AssertionError( + "Wrong fusion mode: expected: " + + fusionModeName(expectedMode) + + ", actual: " + + fusionModeName(establishedFusionMode)); + } + return this; + } + + /** + * Assert that the fusion mode was granted. + * + * @return this + */ + public final AssertSubscriber assertFusionRejected() { + if (establishedFusionMode != Fuseable.NONE) { + throw new AssertionError("Fusion was granted"); + } + return this; + } + + /** + * Assert no error signal has been received. + * + * @return this + */ + public final AssertSubscriber assertNoError() { + int s = errors.size(); + if (s == 1) { + Throwable e = errors.get(0); + String valueAndClass = e == null ? null : e + " (" + e.getClass().getSimpleName() + ")"; + throw new AssertionError("Error present: " + valueAndClass, null); + } + if (s > 1) { + throw new AssertionError("Multiple errors: " + s, null); + } + return this; + } + + /** + * Assert no values have been received. + * + * @return this + */ + public final AssertSubscriber assertNoValues() { + if (valueCount != 0) { + throw new AssertionError( + "No values expected but received: [length = " + values.size() + "] " + values, null); + } + return this; + } + + /** + * Assert that the upstream was not a Fuseable source. + * + * @return this + */ + public final AssertSubscriber assertNonFuseableSource() { + if (qs != null) { + throw new AssertionError("Upstream was Fuseable"); + } + return this; + } + + /** + * Assert no complete successfully signal has been received. + * + * @return this + */ + public final AssertSubscriber assertNotComplete() { + int c = completionCount; + if (c == 1) { + throw new AssertionError("Completed", null); + } + if (c > 1) { + throw new AssertionError("Multiple completions: " + c, null); + } + return this; + } + + /** + * Assert no subscription occurred. + * + * @return this + */ + public final AssertSubscriber assertNotSubscribed() { + int s = subscriptionCount; + + if (s == 1) { + throw new AssertionError("OnSubscribe called once", null); + } + if (s > 1) { + throw new AssertionError("OnSubscribe called multiple times: " + s, null); + } + + return this; + } + + /** + * Assert no complete successfully or error signal has been received. + * + * @return this + */ + public final AssertSubscriber assertNotTerminated() { + if (cdl.getCount() == 0) { + throw new AssertionError("Terminated", null); + } + return this; + } + + /** + * Assert subscription occurred (once). + * + * @return this + */ + public final AssertSubscriber assertSubscribed() { + int s = subscriptionCount; + + if (s == 0) { + throw new AssertionError("OnSubscribe not called", null); + } + if (s > 1) { + throw new AssertionError("OnSubscribe called multiple times: " + s, null); + } + + return this; + } + + /** + * Assert either complete successfully or error signal has been received. + * + * @return this + */ + public final AssertSubscriber assertTerminated() { + if (cdl.getCount() != 0) { + throw new AssertionError("Not terminated", null); + } + return this; + } + + /** + * Assert {@code n} values has been received. + * + * @param n the expected value count + * @return this + */ + public final AssertSubscriber assertValueCount(long n) { + if (valueCount != n) { + throw new AssertionError( + "Different value count: expected = " + n + ", actual = " + valueCount, null); + } + return this; + } + + /** + * Assert the specified values have been received in the same order read by the passed {@link + * Iterable}. Values storage should be enabled to use this method. + * + * @param expectedSequence the values to assert + * @see #configureValuesStorage(boolean) + * @return this + */ + public final AssertSubscriber assertValueSequence(Iterable expectedSequence) { + if (!valuesStorage) { + throw new IllegalStateException("Using assertNoValues() requires enabling values storage"); + } + Iterator actual = values.iterator(); + Iterator expected = expectedSequence.iterator(); + int i = 0; + for (; ; ) { + boolean n1 = actual.hasNext(); + boolean n2 = expected.hasNext(); + if (n1 && n2) { + T t1 = actual.next(); + T t2 = expected.next(); + if (!Objects.equals(t1, t2)) { + throw new AssertionError( + "The element with index " + + i + + " does not match: expected = " + + valueAndClass(t2) + + ", actual = " + + valueAndClass(t1), + null); + } + i++; + } else if (n1 && !n2) { + throw new AssertionError("Actual contains more elements" + values, null); + } else if (!n1 && n2) { + throw new AssertionError("Actual contains fewer elements: " + values, null); + } else { + break; + } + } + return this; + } + + /** + * Assert the specified values have been received in the declared order. Values storage should be + * enabled to use this method. + * + * @param expectedValues the values to assert + * @return this + * @see #configureValuesStorage(boolean) + */ + @SafeVarargs + public final AssertSubscriber assertValues(T... expectedValues) { + return assertValueSequence(Arrays.asList(expectedValues)); + } + + /** + * Assert the specified values have been received in the declared order. Values storage should be + * enabled to use this method. + * + * @param expectations One or more methods that can verify the values and throw a exception (like + * an {@link AssertionError}) if the value is not valid. + * @return this + * @see #configureValuesStorage(boolean) + */ + @SafeVarargs + public final AssertSubscriber assertValuesWith(Consumer... expectations) { + if (!valuesStorage) { + throw new IllegalStateException("Using assertNoValues() requires enabling values storage"); + } + final int expectedValueCount = expectations.length; + if (expectedValueCount != values.size()) { + throw new AssertionError( + "Different value count: expected = " + expectedValueCount + ", actual = " + valueCount, + null); + } + for (int i = 0; i < expectedValueCount; i++) { + Consumer consumer = expectations[i]; + T actualValue = values.get(i); + consumer.accept(actualValue); + } + return this; + } + + // + // ============================================================================================================== + // Await methods + // + // ============================================================================================================== + + /** + * Blocking method that waits until a complete successfully or error signal is received. + * + * @return this + */ + public final AssertSubscriber await() { + if (cdl.getCount() == 0) { + return this; + } + try { + cdl.await(); + } catch (InterruptedException ex) { + throw new AssertionError("Wait interrupted", ex); + } + return this; + } + + /** + * Blocking method that waits until a complete successfully or error signal is received or until a + * timeout occurs. + * + * @param timeout The timeout value + * @return this + */ + public final AssertSubscriber await(Duration timeout) { + if (cdl.getCount() == 0) { + return this; + } + try { + if (!cdl.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) { + throw new AssertionError("No complete or error signal before timeout"); + } + return this; + } catch (InterruptedException ex) { + throw new AssertionError("Wait interrupted", ex); + } + } + + /** + * Blocking method that waits until {@code n} next values have been received. + * + * @param n the value count to assert + * @return this + */ + public final AssertSubscriber awaitAndAssertNextValueCount(final long n) { + await( + valuesTimeout, + () -> { + if (valuesStorage) { + return String.format( + "%d out of %d next values received within %d, " + "values : %s", + valueCount - nextValueAssertedCount, + n, + valuesTimeout.toMillis(), + values.toString()); + } + return String.format( + "%d out of %d next values received within %d", + valueCount - nextValueAssertedCount, n, valuesTimeout.toMillis()); + }, + () -> valueCount >= (nextValueAssertedCount + n)); + nextValueAssertedCount += n; + return this; + } + + /** + * Blocking method that waits until {@code n} next values have been received (n is the number of + * values provided) to assert them. + * + * @param values the values to assert + * @return this + */ + @SafeVarargs + @SuppressWarnings("unchecked") + public final AssertSubscriber awaitAndAssertNextValues(T... values) { + final int expectedNum = values.length; + final List> expectations = new ArrayList<>(); + for (int i = 0; i < expectedNum; i++) { + final T expectedValue = values[i]; + expectations.add( + actualValue -> { + if (!actualValue.equals(expectedValue)) { + throw new AssertionError( + String.format( + "Expected Next signal: %s, but got: %s", expectedValue, actualValue)); + } + }); + } + awaitAndAssertNextValuesWith(expectations.toArray((Consumer[]) new Consumer[0])); + return this; + } + + /** + * Blocking method that waits until {@code n} next values have been received (n is the number of + * expectations provided) to assert them. + * + * @param expectations One or more methods that can verify the values and throw a exception (like + * an {@link AssertionError}) if the value is not valid. + * @return this + */ + @SafeVarargs + public final AssertSubscriber awaitAndAssertNextValuesWith(Consumer... expectations) { + valuesStorage = true; + final int expectedValueCount = expectations.length; + await( + valuesTimeout, + () -> { + if (valuesStorage) { + return String.format( + "%d out of %d next values received within %d, " + "values : %s", + valueCount - nextValueAssertedCount, + expectedValueCount, + valuesTimeout.toMillis(), + values.toString()); + } + return String.format( + "%d out of %d next values received within %d ms", + valueCount - nextValueAssertedCount, expectedValueCount, valuesTimeout.toMillis()); + }, + () -> valueCount >= (nextValueAssertedCount + expectedValueCount)); + List nextValuesSnapshot; + List empty = new ArrayList<>(); + for (; ; ) { + nextValuesSnapshot = values; + if (NEXT_VALUES.compareAndSet(this, values, empty)) { + break; + } + } + if (nextValuesSnapshot.size() < expectedValueCount) { + throw new AssertionError( + String.format( + "Expected %d number of signals but received %d", + expectedValueCount, nextValuesSnapshot.size())); + } + for (int i = 0; i < expectedValueCount; i++) { + Consumer consumer = expectations[i]; + T actualValue = nextValuesSnapshot.get(i); + consumer.accept(actualValue); + } + nextValueAssertedCount += expectedValueCount; + return this; + } + + // + // ============================================================================================================== + // Overrides + // + // ============================================================================================================== + + @Override + public void cancel() { + Subscription a = s; + if (a != Operators.cancelledSubscription()) { + a = S.getAndSet(this, Operators.cancelledSubscription()); + if (a != null && a != Operators.cancelledSubscription()) { + a.cancel(); + } + } + } + + final boolean isCancelled() { + return s == Operators.cancelledSubscription(); + } + + public final boolean isTerminated() { + return cdl.getCount() == 0; + } + + @Override + public void onComplete() { + completionCount++; + cdl.countDown(); + } + + @Override + public void onError(Throwable t) { + errors.add(t); + cdl.countDown(); + } + + @Override + public void onNext(T t) { + if (establishedFusionMode == Fuseable.ASYNC) { + for (; ; ) { + t = qs.poll(); + if (t == null) { + break; + } + valueCount++; + if (valuesStorage) { + List nextValuesSnapshot; + for (; ; ) { + nextValuesSnapshot = values; + nextValuesSnapshot.add(t); + if (NEXT_VALUES.compareAndSet(this, nextValuesSnapshot, nextValuesSnapshot)) { + break; + } + } + } + } + } else { + valueCount++; + if (valuesStorage) { + List nextValuesSnapshot; + for (; ; ) { + nextValuesSnapshot = values; + nextValuesSnapshot.add(t); + if (NEXT_VALUES.compareAndSet(this, nextValuesSnapshot, nextValuesSnapshot)) { + break; + } + } + } + } + } + + @Override + @SuppressWarnings("unchecked") + public void onSubscribe(Subscription s) { + subscriptionCount++; + int requestMode = requestedFusionMode; + if (requestMode >= 0) { + if (!setWithoutRequesting(s)) { + if (!isCancelled()) { + errors.add(new IllegalStateException("Subscription already set: " + subscriptionCount)); + } + } else { + if (s instanceof Fuseable.QueueSubscription) { + this.qs = (Fuseable.QueueSubscription) s; + + int m = qs.requestFusion(requestMode); + establishedFusionMode = m; + + if (m == Fuseable.SYNC) { + for (; ; ) { + T v = qs.poll(); + if (v == null) { + onComplete(); + break; + } + + onNext(v); + } + } else { + requestDeferred(); + } + } else { + requestDeferred(); + } + } + } else { + if (!set(s)) { + if (!isCancelled()) { + errors.add(new IllegalStateException("Subscription already set: " + subscriptionCount)); + } + } + } + } + + @Override + public void request(long n) { + if (Operators.validate(n)) { + if (establishedFusionMode != Fuseable.SYNC) { + normalRequest(n); + } + } + } + + @Override + @NonNull + public Context currentContext() { + return context; + } + + /** + * Setup what fusion mode should be requested from the incoming Subscription if it happens to be + * QueueSubscription + * + * @param requestMode the mode to request, see Fuseable constants + * @return this + */ + public final AssertSubscriber requestedFusionMode(int requestMode) { + this.requestedFusionMode = requestMode; + return this; + } + + public Subscription upstream() { + return s; + } + + // + // ============================================================================================================== + // Non public methods + // + // ============================================================================================================== + + protected final void normalRequest(long n) { + Subscription a = s; + if (a != null) { + a.request(n); + } else { + Operators.addCap(REQUESTED, this, n); + + a = s; + + if (a != null) { + long r = REQUESTED.getAndSet(this, 0L); + + if (r != 0L) { + a.request(r); + } + } + } + } + + /** Requests the deferred amount if not zero. */ + protected final void requestDeferred() { + long r = REQUESTED.getAndSet(this, 0L); + + if (r != 0L) { + s.request(r); + } + } + + /** + * Atomically sets the single subscription and requests the missed amount from it. + * + * @param s + * @return false if this arbiter is cancelled or there was a subscription already set + */ + protected final boolean set(Subscription s) { + Objects.requireNonNull(s, "s"); + Subscription a = this.s; + if (a == Operators.cancelledSubscription()) { + s.cancel(); + return false; + } + if (a != null) { + s.cancel(); + Operators.reportSubscriptionSet(); + return false; + } + + if (S.compareAndSet(this, null, s)) { + + long r = REQUESTED.getAndSet(this, 0L); + + if (r != 0L) { + s.request(r); + } + + return true; + } + + a = this.s; + + if (a != Operators.cancelledSubscription()) { + s.cancel(); + return false; + } + + Operators.reportSubscriptionSet(); + return false; + } + + /** + * Sets the Subscription once but does not request anything. + * + * @param s the Subscription to set + * @return true if successful, false if the current subscription is not null + */ + protected final boolean setWithoutRequesting(Subscription s) { + Objects.requireNonNull(s, "s"); + for (; ; ) { + Subscription a = this.s; + if (a == Operators.cancelledSubscription()) { + s.cancel(); + return false; + } + if (a != null) { + s.cancel(); + Operators.reportSubscriptionSet(); + return false; + } + + if (S.compareAndSet(this, null, s)) { + return true; + } + } + } + + /** + * Prepares and throws an AssertionError exception based on the message, cause, the active state + * and the potential errors so far. + * + * @param message the message + * @param cause the optional Throwable cause + * @throws AssertionError as expected + */ + protected final void assertionError(String message, Throwable cause) { + StringBuilder b = new StringBuilder(); + + if (cdl.getCount() != 0) { + b.append("(active) "); + } + b.append(message); + + List err = errors; + if (!err.isEmpty()) { + b.append(" (+ ").append(err.size()).append(" errors)"); + } + AssertionError e = new AssertionError(b.toString(), cause); + + for (Throwable t : err) { + e.addSuppressed(t); + } + + throw e; + } + + protected final String fusionModeName(int mode) { + switch (mode) { + case -1: + return "Disabled"; + case Fuseable.NONE: + return "None"; + case Fuseable.SYNC: + return "Sync"; + case Fuseable.ASYNC: + return "Async"; + default: + return "Unknown(" + mode + ")"; + } + } + + protected final String valueAndClass(Object o) { + if (o == null) { + return null; + } + return o + " (" + o.getClass().getSimpleName() + ")"; + } + + public List values() { + return values; + } + + public final AssertSubscriber assertNoEvents() { + return assertNoValues().assertNoError().assertNotComplete(); + } + + @SafeVarargs + public final AssertSubscriber assertIncomplete(T... values) { + return assertValues(values).assertNotComplete().assertNoError(); + } +} From d87ae995c6c59e59249c7224e92799352b648adc Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 6 Dec 2019 17:20:43 +0200 Subject: [PATCH 116/181] Feature/perf tunning 3 (#726) * provides improved `UnicastMonoEmpty` Signed-off-by: Oleh Dokuka * provides built-in lifecycle handler for UnicastMonoProcessor Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/RSocketRequester.java | 109 ++--- .../io/rsocket/internal/UnicastMonoEmpty.java | 86 ++++ .../internal/UnicastMonoProcessor.java | 78 +++- .../io/rsocket/util/MonoLifecycleHandler.java | 21 + .../io/rsocket/internal/SchedulerUtils.java | 23 + .../internal/UnicastMonoEmptyTest.java | 97 ++++ .../internal/UnicastMonoProcessorTest.java | 413 ++++++++++++++++-- 7 files changed, 709 insertions(+), 118 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoEmpty.java create mode 100644 rsocket-core/src/main/java/io/rsocket/util/MonoLifecycleHandler.java create mode 100644 rsocket-core/src/test/java/io/rsocket/internal/SchedulerUtils.java create mode 100644 rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoEmptyTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java index cb17ff539..fa44f1131 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java @@ -25,17 +25,28 @@ import io.netty.util.collection.IntObjectMap; import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.exceptions.Exceptions; -import io.rsocket.frame.*; +import io.rsocket.frame.CancelFrameFlyweight; +import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.MetadataPushFrameFlyweight; +import io.rsocket.frame.PayloadFrameFlyweight; +import io.rsocket.frame.RequestChannelFrameFlyweight; +import io.rsocket.frame.RequestFireAndForgetFrameFlyweight; +import io.rsocket.frame.RequestNFrameFlyweight; +import io.rsocket.frame.RequestResponseFrameFlyweight; +import io.rsocket.frame.RequestStreamFrameFlyweight; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.RateLimitableRequestPublisher; import io.rsocket.internal.SynchronizedIntObjectHashMap; import io.rsocket.internal.UnboundedProcessor; +import io.rsocket.internal.UnicastMonoEmpty; import io.rsocket.internal.UnicastMonoProcessor; import io.rsocket.keepalive.KeepAliveFramesAcceptor; import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.keepalive.KeepAliveSupport; import io.rsocket.lease.RequesterLeaseHandler; -import io.rsocket.util.OnceConsumer; +import io.rsocket.util.MonoLifecycleHandler; import java.nio.channels.ClosedChannelException; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; @@ -46,8 +57,11 @@ import org.reactivestreams.Processor; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.publisher.*; +import reactor.core.publisher.BaseSubscriber; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.core.publisher.SignalType; +import reactor.core.publisher.UnicastProcessor; import reactor.util.concurrent.Queues; /** @@ -170,23 +184,19 @@ private Mono handleFireAndForget(Payload payload) { final int streamId = streamIdSupplier.nextStreamId(receivers); - return emptyUnicastMono() - .doOnSubscribe( - new OnceConsumer() { - @Override - public void acceptOnce(@Nonnull Subscription subscription) { - ByteBuf requestFrame = - RequestFireAndForgetFrameFlyweight.encode( - allocator, - streamId, - false, - payload.hasMetadata() ? payload.sliceMetadata().retain() : null, - payload.sliceData().retain()); - payload.release(); - - sendProcessor.onNext(requestFrame); - } - }); + return UnicastMonoEmpty.newInstance( + () -> { + ByteBuf requestFrame = + RequestFireAndForgetFrameFlyweight.encode( + allocator, + streamId, + false, + payload.hasMetadata() ? payload.sliceMetadata().retain() : null, + payload.sliceData().retain()); + payload.release(); + + sendProcessor.onNext(requestFrame); + }); } private Mono handleRequestResponse(final Payload payload) { @@ -199,14 +209,11 @@ private Mono handleRequestResponse(final Payload payload) { int streamId = streamIdSupplier.nextStreamId(receivers); final UnboundedProcessor sendProcessor = this.sendProcessor; - UnicastMonoProcessor receiver = UnicastMonoProcessor.create(); - receivers.put(streamId, receiver); - - return receiver - .doOnSubscribe( - new OnceConsumer() { + UnicastMonoProcessor receiver = + UnicastMonoProcessor.create( + new MonoLifecycleHandler() { @Override - public void acceptOnce(@Nonnull Subscription subscription) { + public void doOnSubscribe() { final ByteBuf requestFrame = RequestResponseFrameFlyweight.encode( allocator, @@ -218,15 +225,23 @@ public void acceptOnce(@Nonnull Subscription subscription) { sendProcessor.onNext(requestFrame); } - }) - .doOnError(t -> sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t))) - .doFinally( - s -> { - if (s == SignalType.CANCEL) { - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + + @Override + public void doOnTerminal( + @Nonnull SignalType signalType, + @Nullable Payload element, + @Nullable Throwable e) { + if (signalType == SignalType.ON_ERROR) { + sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, e)); + } else if (signalType == SignalType.CANCEL) { + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + } + removeStreamReceiver(streamId); } - removeStreamReceiver(streamId); }); + receivers.put(streamId, receiver); + + return receiver; } private Flux handleRequestStream(final Payload payload) { @@ -390,24 +405,14 @@ private Mono handleMetadataPush(Payload payload) { return Mono.error(err); } - return emptyUnicastMono() - .doOnSubscribe( - new OnceConsumer() { - @Override - public void acceptOnce(@Nonnull Subscription subscription) { - ByteBuf metadataPushFrame = - MetadataPushFrameFlyweight.encode(allocator, payload.sliceMetadata().retain()); - payload.release(); - - sendProcessor.onNext(metadataPushFrame); - } - }); - } + return UnicastMonoEmpty.newInstance( + () -> { + ByteBuf metadataPushFrame = + MetadataPushFrameFlyweight.encode(allocator, payload.sliceMetadata().retain()); + payload.release(); - private static UnicastMonoProcessor emptyUnicastMono() { - UnicastMonoProcessor result = UnicastMonoProcessor.create(); - result.onComplete(); - return result; + sendProcessor.onNext(metadataPushFrame); + }); } private Throwable checkAvailable() { diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoEmpty.java b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoEmpty.java new file mode 100644 index 000000000..eb8a1aa11 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoEmpty.java @@ -0,0 +1,86 @@ +package io.rsocket.internal; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import reactor.core.CoreSubscriber; +import reactor.core.Scannable; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.util.annotation.Nullable; + +/** + * Represents an empty publisher which only calls onSubscribe and onComplete. + * + *

This Publisher is effectively stateless and only a single instance exists. Use the {@link + * #instance()} method to obtain a properly type-parametrized view of it. + * + * @see Reactive-Streams-Commons + */ +public final class UnicastMonoEmpty extends Mono implements Scannable { + + final Runnable onSubscribe; + + volatile int once; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater ONCE = + AtomicIntegerFieldUpdater.newUpdater(UnicastMonoEmpty.class, "once"); + + UnicastMonoEmpty(Runnable onSubscribe) { + this.onSubscribe = onSubscribe; + } + + @Override + public void subscribe(CoreSubscriber actual) { + if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { + onSubscribe.run(); + Operators.complete(actual); + } else { + Operators.error( + actual, new IllegalStateException("UnicastMonoEmpty allows only a single Subscriber")); + } + } + + /** + * Returns a properly parametrized instance of this empty Publisher. + * + * @param the output type + * @return a properly parametrized instance of this empty Publisher + */ + @SuppressWarnings("unchecked") + public static Mono newInstance(Runnable onSubscribe) { + return (Mono) new UnicastMonoEmpty(onSubscribe); + } + + @Override + @Nullable + public Object block(Duration m) { + if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { + onSubscribe.run(); + return null; + } else { + throw new IllegalStateException("UnicastMonoEmpty allows only a single Subscriber"); + } + } + + @Override + @Nullable + public Object block() { + if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { + onSubscribe.run(); + return null; + } else { + throw new IllegalStateException("UnicastMonoEmpty allows only a single Subscriber"); + } + } + + @Override + public Object scanUnsafe(Attr key) { + return null; // no particular key to be represented, still useful in hooks + } + + @Override + public String stepName() { + return "source(UnicastMonoEmpty)"; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java index afe72c478..af4c8768b 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java @@ -16,6 +16,7 @@ package io.rsocket.internal; +import io.rsocket.util.MonoLifecycleHandler; import java.util.Objects; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; @@ -29,6 +30,7 @@ import reactor.core.Scannable; import reactor.core.publisher.Mono; import reactor.core.publisher.Operators; +import reactor.core.publisher.SignalType; import reactor.util.annotation.NonNull; import reactor.util.annotation.Nullable; import reactor.util.context.Context; @@ -36,15 +38,30 @@ public class UnicastMonoProcessor extends Mono implements Processor, CoreSubscriber, Disposable, Subscription, Scannable { + static final MonoLifecycleHandler DEFAULT_LIFECYCLE = new MonoLifecycleHandler() {}; + /** * Create a {@link UnicastMonoProcessor} that will eagerly request 1 on {@link - * #onSubscribe(Subscription)}, cache and emit the eventual result for 1 or N subscribers. + * #onSubscribe(Subscription)}, cache and emit the eventual result for a single subscriber. * * @param type of the expected value * @return A {@link UnicastMonoProcessor}. */ + @SuppressWarnings("unchecked") public static UnicastMonoProcessor create() { - return new UnicastMonoProcessor<>(); + return new UnicastMonoProcessor(DEFAULT_LIFECYCLE); + } + + /** + * Create a {@link UnicastMonoProcessor} that will eagerly request 1 on {@link + * #onSubscribe(Subscription)}, cache and emit the eventual result for a single subscriber. + * + * @param lifecycleHandler lifecycle handler + * @param type of the expected value + * @return A {@link UnicastMonoProcessor}. + */ + public static UnicastMonoProcessor create(MonoLifecycleHandler lifecycleHandler) { + return new UnicastMonoProcessor<>(lifecycleHandler); } /** Indicates this Subscription has no value and not requested yet. */ @@ -86,7 +103,11 @@ public static UnicastMonoProcessor create() { Throwable error; O value; - UnicastMonoProcessor() {} + final MonoLifecycleHandler lifecycleHandler; + + UnicastMonoProcessor(MonoLifecycleHandler lifecycleHandler) { + this.lifecycleHandler = lifecycleHandler; + } @Override @NonNull @@ -162,9 +183,11 @@ private void complete(O v) { } if (state == HAS_REQUEST_NO_RESULT) { - final Subscriber a = actual; if (STATE.compareAndSet(this, HAS_REQUEST_NO_RESULT, HAS_REQUEST_HAS_RESULT)) { - this.value = null; + final Subscriber a = actual; + actual = null; + value = null; + lifecycleHandler.doOnTerminal(SignalType.ON_COMPLETE, v, null); a.onNext(v); a.onComplete(); return; @@ -197,8 +220,10 @@ private void complete() { } if (state == HAS_REQUEST_NO_RESULT || state == NO_REQUEST_NO_RESULT) { - final Subscriber a = actual; if (STATE.compareAndSet(this, state, HAS_REQUEST_HAS_RESULT)) { + final Subscriber a = actual; + actual = null; + lifecycleHandler.doOnTerminal(SignalType.ON_COMPLETE, null, null); a.onComplete(); return; } @@ -229,8 +254,10 @@ private void complete(Throwable e) { setError(e); if (state == HAS_REQUEST_NO_RESULT || state == NO_REQUEST_NO_RESULT) { - final Subscriber a = actual; if (STATE.compareAndSet(this, state, HAS_REQUEST_HAS_RESULT)) { + final Subscriber a = actual; + actual = null; + lifecycleHandler.doOnTerminal(SignalType.ON_ERROR, null, e); a.onError(e); return; } @@ -247,10 +274,11 @@ public void subscribe(CoreSubscriber actual) { Objects.requireNonNull(actual, "subscribe"); if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { - this.actual = actual; + final MonoLifecycleHandler lh = this.lifecycleHandler; - // CAS LOOP since there is a racing with [onNext / onComplete / onError / dispose] which can - // appear at any moment + lh.doOnSubscribe(); + + this.actual = actual; int state = this.state; @@ -280,8 +308,10 @@ public void subscribe(CoreSubscriber actual) { // barrier to flush changes STATE.set(this, HAS_REQUEST_HAS_RESULT); if (e == null) { + lh.doOnTerminal(SignalType.ON_COMPLETE, null, null); Operators.complete(actual); } else { + lh.doOnTerminal(SignalType.ON_ERROR, null, e); Operators.error(actual, e); } return; @@ -306,16 +336,17 @@ public final void request(long n) { if ((s & ~NO_REQUEST_HAS_RESULT) != 0) { return; } - if (s == NO_REQUEST_HAS_RESULT - && STATE.compareAndSet(this, NO_REQUEST_HAS_RESULT, HAS_REQUEST_HAS_RESULT)) { - O v = value; - if (v != null) { + if (s == NO_REQUEST_HAS_RESULT) { + if (STATE.compareAndSet(this, NO_REQUEST_HAS_RESULT, HAS_REQUEST_HAS_RESULT)) { + final Subscriber a = actual; + final O v = value; + actual = null; value = null; - Subscriber a = actual; + lifecycleHandler.doOnTerminal(SignalType.ON_COMPLETE, v, null); a.onNext(v); a.onComplete(); + return; } - return; } if (STATE.compareAndSet(this, NO_REQUEST_NO_RESULT, HAS_REQUEST_NO_RESULT)) { return; @@ -328,15 +359,14 @@ public final void request(long n) { public final void cancel() { if (STATE.getAndSet(this, CANCELLED) <= HAS_REQUEST_NO_RESULT) { Operators.onDiscard(value, currentContext()); + value = null; + actual = null; + lifecycleHandler.doOnTerminal(SignalType.CANCEL, null, null); + final Subscription s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription()); + if (s != null && s != Operators.cancelledSubscription()) { + s.cancel(); + } } - - final Subscription s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription()); - if (s != null) { - s.cancel(); - } - - value = null; - actual = null; } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/util/MonoLifecycleHandler.java b/rsocket-core/src/main/java/io/rsocket/util/MonoLifecycleHandler.java new file mode 100644 index 000000000..4d47c03d6 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/MonoLifecycleHandler.java @@ -0,0 +1,21 @@ +package io.rsocket.util; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import reactor.core.publisher.SignalType; + +public interface MonoLifecycleHandler { + + default void doOnSubscribe() {} + + /** + * Handler which is invoked on the terminal activity within a given Monoø + * + * @param signalType a type of signal which explain what happened + * @param element an carried element. May not be present if stream is empty or cancelled or + * errored + * @param e an carried error. May not be present if stream is cancelled or completed successfully + */ + default void doOnTerminal( + @Nonnull SignalType signalType, @Nullable T element, @Nullable Throwable e) {} +} diff --git a/rsocket-core/src/test/java/io/rsocket/internal/SchedulerUtils.java b/rsocket-core/src/test/java/io/rsocket/internal/SchedulerUtils.java new file mode 100644 index 000000000..d73f92b85 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/internal/SchedulerUtils.java @@ -0,0 +1,23 @@ +package io.rsocket.internal; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import reactor.core.scheduler.Scheduler; + +public class SchedulerUtils { + + public static void warmup(Scheduler scheduler) throws InterruptedException { + warmup(scheduler, 10000); + } + + public static void warmup(Scheduler scheduler, int times) throws InterruptedException { + scheduler.start(); + + // warm up + CountDownLatch latch = new CountDownLatch(times); + for (int i = 0; i < times; i++) { + scheduler.schedule(latch::countDown); + } + latch.await(5, TimeUnit.SECONDS); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoEmptyTest.java b/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoEmptyTest.java new file mode 100644 index 000000000..76bb953a4 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoEmptyTest.java @@ -0,0 +1,97 @@ +package io.rsocket.internal; + +import static io.rsocket.internal.SchedulerUtils.warmup; + +import java.time.Duration; +import java.util.concurrent.atomic.AtomicInteger; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; +import reactor.test.util.RaceTestUtils; + +public class UnicastMonoEmptyTest { + + @Test + public void shouldSupportASingleSubscriber() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + AtomicInteger times = new AtomicInteger(); + Mono unicastMono = UnicastMonoEmpty.newInstance(times::incrementAndGet); + + Assertions.assertThatThrownBy( + () -> + RaceTestUtils.race( + unicastMono::subscribe, unicastMono::subscribe, Schedulers.single())) + .hasCause(new IllegalStateException("UnicastMonoEmpty allows only a single Subscriber")); + Assertions.assertThat(times.get()).isEqualTo(1); + } + } + + @Test + public void shouldSupportASingleBlock() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + AtomicInteger times = new AtomicInteger(); + Mono unicastMono = UnicastMonoEmpty.newInstance(times::incrementAndGet); + + Assertions.assertThatThrownBy( + () -> RaceTestUtils.race(unicastMono::block, unicastMono::block, Schedulers.single())) + .hasMessage("UnicastMonoEmpty allows only a single Subscriber"); + Assertions.assertThat(times.get()).isEqualTo(1); + } + } + + @Test + public void shouldSupportASingleBlockWithTimeout() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + AtomicInteger times = new AtomicInteger(); + Mono unicastMono = UnicastMonoEmpty.newInstance(times::incrementAndGet); + + Assertions.assertThatThrownBy( + () -> + RaceTestUtils.race( + () -> unicastMono.block(Duration.ofMinutes(1)), + () -> unicastMono.block(Duration.ofMinutes(1)), + Schedulers.single())) + .hasMessage("UnicastMonoEmpty allows only a single Subscriber"); + Assertions.assertThat(times.get()).isEqualTo(1); + } + } + + @Test + public void shouldSupportASingleSubscribeOrBlock() throws InterruptedException { + warmup(Schedulers.parallel()); + + for (int i = 0; i < 10000; i++) { + AtomicInteger times = new AtomicInteger(); + Mono unicastMono = UnicastMonoEmpty.newInstance(times::incrementAndGet); + + Assertions.assertThatThrownBy( + () -> + RaceTestUtils.race( + unicastMono::subscribe, + () -> + RaceTestUtils.race( + unicastMono::block, + () -> unicastMono.block(Duration.ofMinutes(1)), + Schedulers.parallel()), + Schedulers.parallel())) + .matches( + t -> { + Assertions.assertThat(t.getSuppressed()).hasSize(2); + Assertions.assertThat(t.getSuppressed()[0]) + .hasMessageContaining("UnicastMonoEmpty allows only a single Subscriber"); + Assertions.assertThat(t.getSuppressed()[1]) + .hasMessageContaining("UnicastMonoEmpty allows only a single Subscriber"); + + return true; + }); + Assertions.assertThat(times.get()).isEqualTo(1); + } + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java b/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java index 9e4357bab..a836dd509 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java @@ -16,19 +16,19 @@ package io.rsocket.internal; +import static io.rsocket.internal.SchedulerUtils.warmup; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatThrownBy; import io.rsocket.internal.subscriber.AssertSubscriber; +import io.rsocket.util.MonoLifecycleHandler; import java.lang.ref.WeakReference; import java.time.Duration; import java.util.ArrayList; import java.util.Date; import java.util.concurrent.CancellationException; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; import java.util.concurrent.atomic.AtomicReference; @@ -42,7 +42,7 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.Operators; -import reactor.core.scheduler.Scheduler; +import reactor.core.publisher.SignalType; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; import reactor.test.publisher.TestPublisher; @@ -52,36 +52,128 @@ public class UnicastMonoProcessorTest { + static class VerifyMonoLifecycleHandler implements MonoLifecycleHandler { + private final AtomicInteger onSubscribeCounter = new AtomicInteger(); + private final AtomicInteger onTerminalCounter = new AtomicInteger(); + private final AtomicReference valueReference = new AtomicReference<>(); + private final AtomicReference errorReference = new AtomicReference<>(); + private final AtomicReference signalTypeReference = new AtomicReference<>(); + + @Override + public void doOnSubscribe() { + onSubscribeCounter.incrementAndGet(); + } + + @Override + public void doOnTerminal(SignalType signalType, T element, Throwable e) { + onTerminalCounter.incrementAndGet(); + signalTypeReference.set(signalType); + valueReference.set(element); + errorReference.set(e); + } + + public VerifyMonoLifecycleHandler assertSubscribed() { + assertThat(onSubscribeCounter.get()).isOne(); + return this; + } + + public VerifyMonoLifecycleHandler assertNotSubscribed() { + assertThat(onSubscribeCounter.get()).isZero(); + return this; + } + + public VerifyMonoLifecycleHandler assertTerminated() { + assertThat(onTerminalCounter.get()).describedAs("Expected a single terminal signal").isOne(); + return this; + } + + public VerifyMonoLifecycleHandler assertNotTerminated() { + assertThat(onTerminalCounter.get()).describedAs("Expected zero terminal signals").isZero(); + return this; + } + + public VerifyMonoLifecycleHandler assertCompleted() { + assertTerminated(); + assertThat(signalTypeReference.get()) + .describedAs("Expected ON_COMPLETE signal") + .isEqualTo(SignalType.ON_COMPLETE); + assertThat(errorReference.get()).describedAs("Expected error to be absent").isNull(); + assertThat(valueReference.get()).isNull(); + return this; + } + + public VerifyMonoLifecycleHandler assertCompleted(T value) { + assertTerminated(); + assertThat(signalTypeReference.get()) + .describedAs("Expected ON_COMPLETE signal") + .isEqualTo(SignalType.ON_COMPLETE); + assertThat(errorReference.get()).describedAs("Expected error to be absent").isNull(); + assertThat(valueReference.get()).isEqualTo(value); + return this; + } + + public VerifyMonoLifecycleHandler assertErrored() { + assertTerminated(); + assertThat(signalTypeReference.get()) + .describedAs("Expected ON_ERROR signal") + .isEqualTo(SignalType.ON_ERROR); + assertThat(errorReference.get()).describedAs("Expected error to be present").isNotNull(); + assertThat(valueReference.get()).isNull(); + return this; + } + + public VerifyMonoLifecycleHandler assertCancelled() { + assertTerminated(); + assertThat(signalTypeReference.get()) + .describedAs("Expected ON_ERROR signal") + .isEqualTo(SignalType.CANCEL); + assertThat(errorReference.get()).describedAs("Expected error to be absent").isNull(); + assertThat(valueReference.get()).isNull(); + return this; + } + } + @Test public void testUnicast() throws InterruptedException { warmup(Schedulers.single()); for (int i = 0; i < 10000; i++) { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); + verifyMonoLifecycleHandler.assertNotSubscribed(); assertThatThrownBy(() -> RaceTestUtils.race(processor::subscribe, processor::subscribe)) .hasCause( new IllegalStateException("UnicastMonoProcessor allows only a single Subscriber")); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); } } @Test public void stateFlowTest1_Next() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = AssertSubscriber.create(0); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.onNext(1); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_HAS_RESULT); assertSubscriber.assertNoEvents(); assertSubscriber.request(1); + verifyMonoLifecycleHandler.assertCompleted(1); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertValues(1); @@ -90,17 +182,23 @@ public void stateFlowTest1_Next() { @Test public void stateFlowTest1_Complete() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.onComplete(); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertNoValues(); @@ -109,18 +207,24 @@ public void stateFlowTest1_Complete() { @Test public void stateFlowTest1_Error() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = AssertSubscriber.create(0); RuntimeException testError = new RuntimeException("test"); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.onError(testError); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertNoValues(); @@ -130,17 +234,23 @@ public void stateFlowTest1_Error() { @Test public void stateFlowTest1_Dispose() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.dispose(); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertNoValues(); @@ -150,22 +260,29 @@ public void stateFlowTest1_Dispose() { @Test public void stateFlowTest2_Next() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); processor.onNext(1); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_HAS_RESULT); assertSubscriber.assertNoEvents(); assertSubscriber.request(1); + verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertValues(1); @@ -174,17 +291,23 @@ public void stateFlowTest2_Next() { @Test public void stateFlowTest2_Complete() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); processor.onComplete(); + verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertNoValues(); @@ -193,17 +316,23 @@ public void stateFlowTest2_Complete() { @Test public void stateFlowTest2_Error() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); processor.onError(new RuntimeException("Test")); + verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertNoValues(); @@ -213,17 +342,23 @@ public void stateFlowTest2_Error() { @Test public void stateFlowTest2_Dispose() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); processor.dispose(); + verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertNoValues(); @@ -233,23 +368,30 @@ public void stateFlowTest2_Dispose() { @Test public void stateFlowTest3_Next() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); assertSubscriber.assertNoEvents(); assertSubscriber.request(1); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); assertSubscriber.assertNoEvents(); processor.onNext(1); + verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertValues(1); @@ -258,21 +400,28 @@ public void stateFlowTest3_Next() { @Test public void stateFlowTest3_Complete() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); assertSubscriber.request(1); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); processor.onComplete(); + verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertNoValues(); @@ -281,21 +430,28 @@ public void stateFlowTest3_Complete() { @Test public void stateFlowTest3_Error() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); assertSubscriber.request(1); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); processor.onError(new RuntimeException("Test")); + verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertNoValues(); @@ -305,21 +461,28 @@ public void stateFlowTest3_Error() { @Test public void stateFlowTest3_Dispose() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = AssertSubscriber.create(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); assertSubscriber.request(1); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); processor.dispose(); + verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertNoValues(); @@ -330,30 +493,38 @@ public void stateFlowTest3_Dispose() { @Test public void stateFlowTest4_Next() { ArrayList discarded = new ArrayList<>(); - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); // Context discardingContext = Operators.enableOnDiscard(null, discarded::add); Hooks.onNextDropped(discarded::add); AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); try { + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); assertSubscriber.request(1); assertSubscriber.assertNoEvents(); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); assertSubscriber.cancel(); assertSubscriber.assertNoEvents(); + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); processor.onNext(1); + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); assertSubscriber.assertNoEvents(); @@ -366,32 +537,40 @@ public void stateFlowTest4_Next() { @Test public void stateFlowTest4_Error() { ArrayList discarded = new ArrayList<>(); - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); // Context discardingContext = Operators.enableOnDiscard(null, discarded::add); Hooks.onErrorDropped(discarded::add); AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); try { + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); assertSubscriber.request(1); assertSubscriber.assertNoEvents(); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); assertSubscriber.cancel(); assertSubscriber.assertNoEvents(); + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); RuntimeException testError = new RuntimeException("test"); processor.onError(testError); + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); assertSubscriber.assertNoEvents(); @@ -405,30 +584,38 @@ public void stateFlowTest4_Error() { @Test public void stateFlowTest4_Dispose() { ArrayList discarded = new ArrayList<>(); - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); // Context discardingContext = Operators.enableOnDiscard(null, discarded::add); Hooks.onErrorDropped(discarded::add); try { AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); assertSubscriber.request(1); assertSubscriber.assertNoEvents(); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); assertSubscriber.cancel(); assertSubscriber.assertNoEvents(); + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); processor.dispose(); + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); assertSubscriber.assertNoEvents(); @@ -441,30 +628,38 @@ public void stateFlowTest4_Dispose() { @Test public void stateFlowTest4_Complete() { ArrayList discarded = new ArrayList<>(); - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); // Context discardingContext = Operators.enableOnDiscard(null, discarded::add); Hooks.onErrorDropped(discarded::add); AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); try { + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); assertSubscriber.request(1); assertSubscriber.assertNoEvents(); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); assertSubscriber.cancel(); assertSubscriber.assertNoEvents(); + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); processor.onComplete(); + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); assertSubscriber.assertNoEvents(); @@ -477,22 +672,34 @@ public void stateFlowTest4_Complete() { @Test public void stateFlowTest5_Next() { ArrayList discarded = new ArrayList<>(); - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); Context discardingContext = Operators.enableOnDiscard(null, discarded::add); AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); processor.onNext(1); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_HAS_RESULT); assertSubscriber.cancel(); + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + assertSubscriber.request(1); + + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); assertSubscriber.assertNoEvents(); @@ -502,22 +709,29 @@ public void stateFlowTest5_Next() { @Test public void stateFlowTest5_Complete() { ArrayList discarded = new ArrayList<>(); - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); Context discardingContext = Operators.enableOnDiscard(null, discarded::add); AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); processor.onComplete(); + verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.cancel(); + verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); assertSubscriber.assertComplete(); @@ -527,24 +741,31 @@ public void stateFlowTest5_Complete() { @Test public void stateFlowTest5_Error() { ArrayList discarded = new ArrayList<>(); - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); Context discardingContext = Operators.enableOnDiscard(null, discarded::add); Hooks.onErrorDropped(discarded::add); AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); try { + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); processor.onError(new RuntimeException("test")); + verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.cancel(); + verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); assertSubscriber.assertError(RuntimeException.class); @@ -558,24 +779,31 @@ public void stateFlowTest5_Error() { @Test public void stateFlowTest5_Dispose() { ArrayList discarded = new ArrayList<>(); - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); Context discardingContext = Operators.enableOnDiscard(null, discarded::add); Hooks.onErrorDropped(discarded::add); AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); try { + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); processor.dispose(); + verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.cancel(); + verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); assertSubscriber.assertError(CancellationException.class); @@ -589,22 +817,34 @@ public void stateFlowTest5_Dispose() { @Test public void stateFlowTest6_Next() { ArrayList discarded = new ArrayList<>(); - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); Context discardingContext = Operators.enableOnDiscard(null, discarded::add); AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.onNext(1); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_HAS_RESULT); assertSubscriber.cancel(); + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + assertSubscriber.request(1); + + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); assertSubscriber.assertNoEvents(); @@ -616,9 +856,13 @@ public void stateFlowTest7_Next() throws InterruptedException { warmup(Schedulers.single()); for (int i = 0; i < 10000; i++) { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = new AssertSubscriber<>(); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); RaceTestUtils.race( @@ -626,6 +870,7 @@ public void stateFlowTest7_Next() throws InterruptedException { () -> processor.subscribe(assertSubscriber), Schedulers.single()); + verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertValues(1); @@ -681,14 +926,19 @@ public void stateFlowTest7_Dispose() throws InterruptedException { warmup(Schedulers.single()); for (int i = 0; i < 10000; i++) { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); RaceTestUtils.race( processor::dispose, () -> processor.subscribe(assertSubscriber), Schedulers.single()); + verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertNoValues(); @@ -702,22 +952,30 @@ public void stateFlowTest8_Next() throws InterruptedException { warmup(Schedulers.single()); for (int i = 0; i < 10000; i++) { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = new AssertSubscriber<>(); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); RaceTestUtils.race(() -> processor.onNext(1), assertSubscriber::cancel, Schedulers.single()); + verifyMonoLifecycleHandler.assertSubscribed().assertTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); if (assertSubscriber.values().isEmpty()) { + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); assertSubscriber.assertNoEvents(); } else { + verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); assertSubscriber.assertValues(1); assertSubscriber.assertComplete(); } @@ -729,23 +987,31 @@ public void stateFlowTest9_Next() throws InterruptedException { warmup(Schedulers.single()); for (int i = 0; i < 10000; i++) { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = new AssertSubscriber<>(); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); RaceTestUtils.race(() -> processor.onNext(1), processor::dispose, Schedulers.single()); + verifyMonoLifecycleHandler.assertSubscribed().assertTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); if (processor.isError()) { + verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); assertSubscriber.assertNoValues(); assertSubscriber.assertErrorMessage("Disposed"); } else { + verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); assertSubscriber.assertValues(1); assertSubscriber.assertComplete(); } @@ -757,19 +1023,26 @@ public void stateFlowTest13_Next() throws InterruptedException { warmup(Schedulers.single()); for (int i = 0; i < 10000; i++) { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.onNext(1); processor.subscribe(assertSubscriber); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); + RaceTestUtils.race( () -> assertSubscriber.request(1), () -> assertSubscriber.request(1), Schedulers.single()); + verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertValues(1); @@ -782,16 +1055,22 @@ public void stateFlowTest14_Next() throws InterruptedException { warmup(Schedulers.single()); for (int i = 0; i < 10000; i++) { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); processor.subscribe(assertSubscriber); assertSubscriber.request(1); + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); RaceTestUtils.race(() -> processor.onNext(1), () -> processor.onNext(1), Schedulers.single()); + verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); assertSubscriber.assertValues(1); @@ -799,6 +1078,67 @@ public void stateFlowTest14_Next() throws InterruptedException { } } + @Test + public void stateFlowTest15_Next() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + processor.onNext(1); + + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); + RaceTestUtils.race( + () -> assertSubscriber.request(1), assertSubscriber::cancel, Schedulers.single()); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + if (assertSubscriber.values().isEmpty()) { + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); + assertSubscriber.assertNoEvents(); + } else { + verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); + assertSubscriber.assertValues(1); + assertSubscriber.assertComplete(); + } + } + } + + @Test + public void stateFlowTest16_Next() throws InterruptedException { + warmup(Schedulers.single()); + + for (int i = 0; i < 10000; i++) { + VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = + new VerifyMonoLifecycleHandler<>(); + UnicastMonoProcessor processor = + UnicastMonoProcessor.create(verifyMonoLifecycleHandler); + AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); + + verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); + + processor.subscribe(assertSubscriber); + processor.onNext(1); + + verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); + RaceTestUtils.race(assertSubscriber::cancel, assertSubscriber::cancel, Schedulers.single()); + + assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); + + verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); + assertSubscriber.assertNoEvents(); + } + } + @Test public void noRetentionOnTermination() throws InterruptedException { Date date = new Date(); @@ -1437,15 +1777,4 @@ public void disposeBeforeValueSendsCancellationException() { processor.subscribe(v -> Assertions.fail("expected late subscriber to error"), late::set); assertThat(late.get()).isInstanceOf(IllegalStateException.class); } - - static void warmup(Scheduler scheduler) throws InterruptedException { - scheduler.start(); - - // warm up - CountDownLatch latch = new CountDownLatch(10000); - for (int i = 0; i < 10000; i++) { - scheduler.schedule(latch::countDown); - } - latch.await(5, TimeUnit.SECONDS); - } } From 9bc8032ab6e0c6fca14ebd4aca999bff48fdd7c0 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 7 Dec 2019 18:16:13 +0200 Subject: [PATCH 117/181] fixes error handling to match specification (#727) Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/RSocketRequester.java | 4 +- .../exceptions/CustomRSocketException.java | 51 ++++++ .../io/rsocket/exceptions/Exceptions.java | 64 ++++--- .../io/rsocket/frame/ErrorFrameFlyweight.java | 6 +- .../main/java/io/rsocket/frame/ErrorType.java | 11 +- .../java/io/rsocket/RSocketLeaseTest.java | 3 +- .../src/test/java/io/rsocket/RSocketTest.java | 30 +++ .../java/io/rsocket/SetupRejectionTest.java | 2 +- .../io/rsocket/exceptions/ExceptionsTest.java | 171 +++++++++++++----- .../exceptions/TestRSocketException.java | 39 ++++ 10 files changed, 309 insertions(+), 72 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java create mode 100644 rsocket-core/src/test/java/io/rsocket/exceptions/TestRSocketException.java diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java index fa44f1131..5590a9df0 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java @@ -475,7 +475,7 @@ private void handleFrame(int streamId, FrameType type, ByteBuf frame) { } else { switch (type) { case ERROR: - receiver.onError(Exceptions.from(frame)); + receiver.onError(Exceptions.from(streamId, frame)); receivers.remove(streamId); break; case NEXT_COMPLETE: @@ -549,7 +549,7 @@ private void tryTerminateOnConnectionClose() { } private void tryTerminateOnZeroError(ByteBuf errorFrame) { - tryTerminate(() -> Exceptions.from(errorFrame)); + tryTerminate(() -> Exceptions.from(0, errorFrame)); } private void tryTerminate(Supplier errorSupplier) { diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java new file mode 100644 index 000000000..6315206b5 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java @@ -0,0 +1,51 @@ +package io.rsocket.exceptions; + +import io.rsocket.frame.ErrorType; + +public class CustomRSocketException extends RSocketException { + private static final long serialVersionUID = 7873267740343446585L; + + private final int errorCode; + + /** + * Constructs a new exception with the specified message. + * + * @param errorCode customizable error code. Should be in range [0x00000301-0xFFFFFFFE] + * @param message the message + * @throws NullPointerException if {@code message} is {@code null} + * @throws IllegalArgumentException if {@code errorCode} is out of allowed range + */ + public CustomRSocketException(int errorCode, String message) { + super(message); + if (errorCode > ErrorType.MAX_USER_ALLOWED_ERROR_CODE + && errorCode < ErrorType.MIN_USER_ALLOWED_ERROR_CODE) { + throw new IllegalArgumentException( + "Allowed errorCode value should be in range [0x00000301-0xFFFFFFFE]"); + } + this.errorCode = errorCode; + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param errorCode customizable error code. Should be in range [0x00000301-0xFFFFFFFE] + * @param message the message + * @param cause the cause of this exception + * @throws NullPointerException if {@code message} or {@code cause} is {@code null} + * @throws IllegalArgumentException if {@code errorCode} is out of allowed range + */ + public CustomRSocketException(int errorCode, String message, Throwable cause) { + super(message, cause); + if (errorCode > ErrorType.MAX_USER_ALLOWED_ERROR_CODE + && errorCode < ErrorType.MIN_USER_ALLOWED_ERROR_CODE) { + throw new IllegalArgumentException( + "Allowed errorCode value should be in range [0x00000301-0xFFFFFFFE]"); + } + this.errorCode = errorCode; + } + + @Override + public int errorCode() { + return errorCode; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java b/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java index 97de65a96..3a10410f0 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java @@ -34,36 +34,50 @@ private Exceptions() {} * @return a {@link RSocketException} that matches the error code in the Frame * @throws NullPointerException if {@code frame} is {@code null} */ - public static RuntimeException from(ByteBuf frame) { + public static RuntimeException from(int streamId, ByteBuf frame) { Objects.requireNonNull(frame, "frame must not be null"); int errorCode = ErrorFrameFlyweight.errorCode(frame); String message = ErrorFrameFlyweight.dataUtf8(frame); - switch (errorCode) { - case APPLICATION_ERROR: - return new ApplicationErrorException(message); - case CANCELED: - return new CanceledException(message); - case CONNECTION_CLOSE: - return new ConnectionCloseException(message); - case CONNECTION_ERROR: - return new ConnectionErrorException(message); - case INVALID: - return new InvalidException(message); - case INVALID_SETUP: - return new InvalidSetupException(message); - case REJECTED: - return new RejectedException(message); - case REJECTED_RESUME: - return new RejectedResumeException(message); - case REJECTED_SETUP: - return new RejectedSetupException(message); - case UNSUPPORTED_SETUP: - return new UnsupportedSetupException(message); - default: - return new IllegalArgumentException( - String.format("Invalid Error frame: %d '%s'", errorCode, message)); + if (streamId == 0) { + switch (errorCode) { + case INVALID_SETUP: + return new InvalidSetupException(message); + case UNSUPPORTED_SETUP: + return new UnsupportedSetupException(message); + case REJECTED_SETUP: + return new RejectedSetupException(message); + case REJECTED_RESUME: + return new RejectedResumeException(message); + case CONNECTION_ERROR: + return new ConnectionErrorException(message); + case CONNECTION_CLOSE: + return new ConnectionCloseException(message); + default: + return new IllegalArgumentException( + String.format("Invalid Error frame in Stream ID 0: 0x%08X '%s'", errorCode, message)); + } + } else { + switch (errorCode) { + case APPLICATION_ERROR: + return new ApplicationErrorException(message); + case REJECTED: + return new RejectedException(message); + case CANCELED: + return new CanceledException(message); + case INVALID: + return new InvalidException(message); + default: + if (errorCode >= MIN_USER_ALLOWED_ERROR_CODE + || errorCode <= MAX_USER_ALLOWED_ERROR_CODE) { + return new CustomRSocketException(errorCode, message); + } + return new IllegalArgumentException( + String.format( + "Invalid Error frame in Stream ID %d: 0x%08X '%s'", + streamId, errorCode, message)); + } } } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java index 55e23541e..df9d39ba8 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java @@ -8,17 +8,21 @@ public class ErrorFrameFlyweight { - // defined error codes + // defined zero stream id error codes public static final int INVALID_SETUP = 0x00000001; public static final int UNSUPPORTED_SETUP = 0x00000002; public static final int REJECTED_SETUP = 0x00000003; public static final int REJECTED_RESUME = 0x00000004; public static final int CONNECTION_ERROR = 0x00000101; public static final int CONNECTION_CLOSE = 0x00000102; + // defined non-zero stream id error codes public static final int APPLICATION_ERROR = 0x00000201; public static final int REJECTED = 0x00000202; public static final int CANCELED = 0x00000203; public static final int INVALID = 0x00000204; + // defined user-allowed error codes range + public static final int MIN_USER_ALLOWED_ERROR_CODE = 0x00000301; + public static final int MAX_USER_ALLOWED_ERROR_CODE = 0xFFFFFFFE; public static ByteBuf encode( ByteBufAllocator allocator, int streamId, Throwable t, ByteBuf data) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ErrorType.java b/rsocket-core/src/main/java/io/rsocket/frame/ErrorType.java index ccbff374e..ffd99930d 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/ErrorType.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/ErrorType.java @@ -11,7 +11,7 @@ public final class ErrorType { /** * Application layer logic generating a Reactive Streams onError event. Stream ID MUST be > 0. */ - public static final int APPLICATION_ERROR = 0x00000201;; + public static final int APPLICATION_ERROR = 0x00000201; /** * The Responder canceled the request but may have started processing it (similar to REJECTED but @@ -70,5 +70,14 @@ public final class ErrorType { */ public static final int UNSUPPORTED_SETUP = 0x00000002; + /** Minimum allowed user defined error code value */ + public static final int MIN_USER_ALLOWED_ERROR_CODE = 0x00000301; + + /** + * Maximum allowed user defined error code value. Note, the value is above signed integer maximum, + * so it will be negative after overflow. + */ + public static final int MAX_USER_ALLOWED_ERROR_CODE = 0xFFFFFFFE; + private ErrorType() {} } diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java index 2a2567843..3af8916cd 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java @@ -141,7 +141,8 @@ public void serverRSocketFactoryRejectsUnsupportedLease() { Assertions.assertThat(sent).hasSize(1); ByteBuf error = sent.iterator().next(); Assertions.assertThat(FrameHeaderFlyweight.frameType(error)).isEqualTo(ERROR); - Assertions.assertThat(Exceptions.from(error).getMessage()).isEqualTo("lease is not supported"); + Assertions.assertThat(Exceptions.from(0, error).getMessage()) + .isEqualTo("lease is not supported"); } @Test diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketTest.java index 5d9672fb9..80865ec47 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/RSocketTest.java @@ -24,6 +24,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.exceptions.ApplicationErrorException; +import io.rsocket.exceptions.CustomRSocketException; import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.lease.ResponderLeaseHandler; import io.rsocket.test.util.LocalDuplexConnection; @@ -38,6 +39,7 @@ import org.junit.rules.ExternalResource; import org.junit.runner.Description; import org.junit.runners.model.Statement; +import org.mockito.ArgumentCaptor; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import reactor.core.publisher.DirectProcessor; @@ -86,6 +88,34 @@ public Mono requestResponse(Payload payload) { rule.assertServerError("java.lang.NullPointerException: Deliberate exception."); } + @Test(timeout = 2000) + public void testHandlerEmitsCustomError() { + rule.setRequestAcceptor( + new AbstractRSocket() { + @Override + public Mono requestResponse(Payload payload) { + return Mono.error( + new CustomRSocketException(0x00000501, "Deliberate Custom exception.")); + } + }); + Subscriber subscriber = TestSubscriber.create(); + rule.crs.requestResponse(EmptyPayload.INSTANCE).subscribe(subscriber); + ArgumentCaptor customRSocketExceptionArgumentCaptor = + ArgumentCaptor.forClass(CustomRSocketException.class); + verify(subscriber).onError(customRSocketExceptionArgumentCaptor.capture()); + + Assert.assertEquals( + "Deliberate Custom exception.", + customRSocketExceptionArgumentCaptor.getValue().getMessage()); + Assert.assertEquals(0x00000501, customRSocketExceptionArgumentCaptor.getValue().errorCode()); + + // Client sees error through normal API + rule.assertNoClientErrors(); + + rule.assertServerError( + "io.rsocket.exceptions.CustomRSocketException: Deliberate Custom exception."); + } + @Test(timeout = 2000) public void testStream() throws Exception { Flux responses = rule.crs.requestStream(DefaultPayload.create("Payload In")); diff --git a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java index cce53a2f2..e6972eec0 100644 --- a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java @@ -37,7 +37,7 @@ void responderRejectSetup() { ByteBuf sentFrame = transport.awaitSent(); assertThat(FrameHeaderFlyweight.frameType(sentFrame)).isEqualTo(FrameType.ERROR); - RuntimeException error = Exceptions.from(sentFrame); + RuntimeException error = Exceptions.from(0, sentFrame); assertThat(errorMsg).isEqualTo(error.getMessage()); assertThat(error).isInstanceOf(RejectedSetupException.class); RSocket acceptorSender = acceptor.senderRSocket().block(); diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java index c7bbfadf6..e646080c7 100644 --- a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java @@ -16,131 +16,220 @@ package io.rsocket.exceptions; +import static io.rsocket.frame.ErrorFrameFlyweight.APPLICATION_ERROR; +import static io.rsocket.frame.ErrorFrameFlyweight.CANCELED; +import static io.rsocket.frame.ErrorFrameFlyweight.CONNECTION_CLOSE; +import static io.rsocket.frame.ErrorFrameFlyweight.CONNECTION_ERROR; +import static io.rsocket.frame.ErrorFrameFlyweight.INVALID; +import static io.rsocket.frame.ErrorFrameFlyweight.INVALID_SETUP; +import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED; +import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED_RESUME; +import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED_SETUP; +import static io.rsocket.frame.ErrorFrameFlyweight.UNSUPPORTED_SETUP; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNullPointerException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.UnpooledByteBufAllocator; +import io.rsocket.frame.ErrorFrameFlyweight; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + final class ExceptionsTest { - /* @DisplayName("from returns ApplicationErrorException") @Test void fromApplicationException() { - ByteBuf byteBuf = createErrorFrame(APPLICATION_ERROR, "test-message"); + ByteBuf byteBuf = createErrorFrame(1, APPLICATION_ERROR, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(1, byteBuf)) .isInstanceOf(ApplicationErrorException.class) - .withFailMessage("test-message"); + .hasMessage("test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Invalid Error frame in Stream ID 0: 0x%08X '%s'", APPLICATION_ERROR, "test-message"); } @DisplayName("from returns CanceledException") @Test void fromCanceledException() { - ByteBuf byteBuf = createErrorFrame(CANCELED, "test-message"); + ByteBuf byteBuf = createErrorFrame(1, CANCELED, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(1, byteBuf)) .isInstanceOf(CanceledException.class) - .withFailMessage("test-message"); + .hasMessage("test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", CANCELED, "test-message"); } @DisplayName("from returns ConnectionCloseException") @Test void fromConnectionCloseException() { - ByteBuf byteBuf = createErrorFrame(CONNECTION_CLOSE, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, CONNECTION_CLOSE, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(0, byteBuf)) .isInstanceOf(ConnectionCloseException.class) - .withFailMessage("test-message"); + .hasMessage("test-message"); + + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", CONNECTION_CLOSE, "test-message"); } @DisplayName("from returns ConnectionErrorException") @Test void fromConnectionErrorException() { - ByteBuf byteBuf = createErrorFrame(CONNECTION_ERROR, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, CONNECTION_ERROR, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(0, byteBuf)) .isInstanceOf(ConnectionErrorException.class) - .withFailMessage("test-message"); + .hasMessage("test-message"); + + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", CONNECTION_ERROR, "test-message"); } @DisplayName("from returns IllegalArgumentException if error frame has illegal error code") @Test void fromIllegalErrorFrame() { - ByteBuf byteBuf = createErrorFrame(0x00000000, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, 0x00000000, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) - .isInstanceOf(IllegalArgumentException.class) - .withFailMessage("Invalid Error frame: %d, '%s'", 0, "test-message"); + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", 0, "test-message") + .isInstanceOf(IllegalArgumentException.class); + + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 1: 0x%08X '%s'", 0x00000000, "test-message") + .isInstanceOf(IllegalArgumentException.class); } @DisplayName("from returns InvalidException") @Test void fromInvalidException() { - ByteBuf byteBuf = createErrorFrame(INVALID, "test-message"); + ByteBuf byteBuf = createErrorFrame(1, INVALID, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(1, byteBuf)) .isInstanceOf(InvalidException.class) - .withFailMessage("test-message"); + .hasMessage("test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", INVALID, "test-message") + .isInstanceOf(IllegalArgumentException.class); } @DisplayName("from returns InvalidSetupException") @Test void fromInvalidSetupException() { - ByteBuf byteBuf = createErrorFrame(INVALID_SETUP, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, INVALID_SETUP, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(0, byteBuf)) .isInstanceOf(InvalidSetupException.class) - .withFailMessage("test-message"); + .hasMessage("test-message"); + + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", INVALID_SETUP, "test-message") + .isInstanceOf(IllegalArgumentException.class); } @DisplayName("from returns RejectedException") @Test void fromRejectedException() { - ByteBuf byteBuf = createErrorFrame(REJECTED, "test-message"); + ByteBuf byteBuf = createErrorFrame(1, REJECTED, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(1, byteBuf)) .isInstanceOf(RejectedException.class) .withFailMessage("test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", REJECTED, "test-message") + .isInstanceOf(IllegalArgumentException.class); } @DisplayName("from returns RejectedResumeException") @Test void fromRejectedResumeException() { - ByteBuf byteBuf = createErrorFrame(REJECTED_RESUME, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, REJECTED_RESUME, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(0, byteBuf)) .isInstanceOf(RejectedResumeException.class) - .withFailMessage("test-message"); + .hasMessage("test-message"); + + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", REJECTED_RESUME, "test-message") + .isInstanceOf(IllegalArgumentException.class); } @DisplayName("from returns RejectedSetupException") @Test void fromRejectedSetupException() { - ByteBuf byteBuf = createErrorFrame(REJECTED_SETUP, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, REJECTED_SETUP, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(0, byteBuf)) .isInstanceOf(RejectedSetupException.class) .withFailMessage("test-message"); + + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", REJECTED_SETUP, "test-message") + .isInstanceOf(IllegalArgumentException.class); } @DisplayName("from returns UnsupportedSetupException") @Test void fromUnsupportedSetupException() { - ByteBuf byteBuf = createErrorFrame(UNSUPPORTED_SETUP, "test-message"); + ByteBuf byteBuf = createErrorFrame(0, UNSUPPORTED_SETUP, "test-message"); - assertThat(Exceptions.from(Frame.from(byteBuf))) + assertThat(Exceptions.from(0, byteBuf)) .isInstanceOf(UnsupportedSetupException.class) - .withFailMessage("test-message"); + .hasMessage("test-message"); + + assertThat(Exceptions.from(1, byteBuf)) + .hasMessage( + "Invalid Error frame in Stream ID 1: 0x%08X '%s'", UNSUPPORTED_SETUP, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("from returns CustomRSocketException") + @Test + void fromCustomRSocketException() { + for (int i = 0; i < 1000; i++) { + int randomCode = + ThreadLocalRandom.current().nextBoolean() + ? ThreadLocalRandom.current() + .nextInt(Integer.MIN_VALUE, ErrorFrameFlyweight.MAX_USER_ALLOWED_ERROR_CODE) + : ThreadLocalRandom.current() + .nextInt(ErrorFrameFlyweight.MIN_USER_ALLOWED_ERROR_CODE, Integer.MAX_VALUE); + ByteBuf byteBuf = createErrorFrame(0, randomCode, "test-message"); + + assertThat(Exceptions.from(1, byteBuf)) + .isInstanceOf(CustomRSocketException.class) + .hasMessage("test-message"); + + assertThat(Exceptions.from(0, byteBuf)) + .hasMessage("Invalid Error frame in Stream ID 0: 0x%08X '%s'", randomCode, "test-message") + .isInstanceOf(IllegalArgumentException.class); + } } @DisplayName("from throws NullPointerException with null frame") @Test void fromWithNullFrame() { assertThatNullPointerException() - .isThrownBy(() -> Exceptions.from(null)) + .isThrownBy(() -> Exceptions.from(0, null)) .withMessage("frame must not be null"); } - private ByteBuf createErrorFrame(int errorCode, String message) { - ByteBuf byteBuf = Unpooled.buffer(); - - ErrorFrameFlyweight.encode(byteBuf, 0, errorCode, Unpooled.copiedBuffer(message, UTF_8)); - - return byteBuf; - }*/ + private ByteBuf createErrorFrame(int streamId, int errorCode, String message) { + return ErrorFrameFlyweight.encode( + UnpooledByteBufAllocator.DEFAULT, streamId, new TestRSocketException(errorCode, message)); + } } diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/TestRSocketException.java b/rsocket-core/src/test/java/io/rsocket/exceptions/TestRSocketException.java new file mode 100644 index 000000000..6c2e63730 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/TestRSocketException.java @@ -0,0 +1,39 @@ +package io.rsocket.exceptions; + +public class TestRSocketException extends RSocketException { + private static final long serialVersionUID = 7873267740343446585L; + + private final int errorCode; + + /** + * Constructs a new exception with the specified message. + * + * @param errorCode customizable error code + * @param message the message + * @throws NullPointerException if {@code message} is {@code null} + * @throws IllegalArgumentException if {@code errorCode} is out of allowed range + */ + public TestRSocketException(int errorCode, String message) { + super(message); + this.errorCode = errorCode; + } + + /** + * Constructs a new exception with the specified message and cause. + * + * @param errorCode customizable error code + * @param message the message + * @param cause the cause of this exception + * @throws NullPointerException if {@code message} or {@code cause} is {@code null} + * @throws IllegalArgumentException if {@code errorCode} is out of allowed range + */ + public TestRSocketException(int errorCode, String message, Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + } + + @Override + public int errorCode() { + return errorCode; + } +} From ca136e88aa8af34f4597574f4c21210e658fa2a1 Mon Sep 17 00:00:00 2001 From: Jacky Chan Date: Thu, 19 Dec 2019 04:47:14 +0800 Subject: [PATCH 118/181] Add MESSAGE_RSOCKET_AUTHENTICATION (#728) According to specification from https://github.com/rsocket/rsocket/blob/master/Extensions/Security/Authentication.md Signed-off-by: linux_china --- .../src/main/java/io/rsocket/metadata/WellKnownMimeType.java | 1 + 1 file changed, 1 insertion(+) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java index 82cce54a0..2743f604d 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java @@ -73,6 +73,7 @@ public enum WellKnownMimeType { // ... reserved for future use ... + MESSAGE_RSOCKET_AUTHENTICATION("message/x.rsocket.authentication.v0", (byte) 0x7C), MESSAGE_RSOCKET_TRACING_ZIPKIN("message/x.rsocket.tracing-zipkin.v0", (byte) 0x7D), MESSAGE_RSOCKET_ROUTING("message/x.rsocket.routing.v0", (byte) 0x7E), MESSAGE_RSOCKET_COMPOSITE_METADATA("message/x.rsocket.composite-metadata.v0", (byte) 0x7F); From 4e966851cd2ea902b2ee02bd2e3e6871e6f1d705 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 20 Dec 2019 15:43:05 +0200 Subject: [PATCH 119/181] fixes brunch artifacts naming --- ci/travis.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/travis.sh b/ci/travis.sh index 411f4418b..d190a59ec 100755 --- a/ci/travis.sh +++ b/ci/travis.sh @@ -21,7 +21,7 @@ elif [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_TAG" == "" ] && [ "$bin ./gradlew \ -PbintrayUser="${bintrayUser}" -PbintrayKey="${bintrayKey}" \ -PsonatypeUsername="${sonatypeUsername}" -PsonatypePassword="${sonatypePassword}" \ - -PversionSuffix="-$TRAVIS_BRANCH-SNAPSHOT" \ + -PversionSuffix="-${TRAVIS_BRANCH//\//-}-SNAPSHOT" \ -PbuildNumber="$TRAVIS_BUILD_NUMBER" \ build artifactoryPublish --stacktrace From 7f76f0aa90c256d0e03cf0be54e583c04aa16b65 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 27 Jan 2020 22:23:38 +0200 Subject: [PATCH 120/181] Authentication Metadata Extension support (#731) * fixes tuples bytebufs Signed-off-by: Oleh Dokuka * provides first draft implementation of AuthMetadataFlyweight right now it supports encoding only Signed-off-by: Oleh Dokuka * provides full implementation of AuthMetadataFlyweight Signed-off-by: Oleh Dokuka * provides draft of AuthMetadata Signed-off-by: Oleh Dokuka * fixes allowed username max length Signed-off-by: Oleh Dokuka * removes eager release Signed-off-by: Oleh Dokuka * fixes formating Signed-off-by: Oleh Dokuka * provides minor fixes Signed-off-by: Oleh Dokuka --- .../rsocket/buffer/AbstractTupleByteBuf.java | 2 +- .../java/io/rsocket/buffer/Tuple2ByteBuf.java | 24 +- .../java/io/rsocket/buffer/Tuple3ByteBuf.java | 12 +- .../security/AuthMetadataFlyweight.java | 336 +++++++++++++ .../metadata/security/WellKnownAuthType.java | 121 +++++ .../java/io/rsocket/util/CharByteBufUtil.java | 208 ++++++++ .../security/AuthMetadataFlyweightTest.java | 470 ++++++++++++++++++ 7 files changed, 1151 insertions(+), 22 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/security/WellKnownAuthType.java create mode 100644 rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java create mode 100644 rsocket-core/src/test/java/io/rsocket/metadata/security/AuthMetadataFlyweightTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java index fbac4b1a0..a80605877 100644 --- a/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java +++ b/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java @@ -550,7 +550,7 @@ public int writeCharSequence(CharSequence sequence, Charset charset) { @Override public ByteBuffer internalNioBuffer(int index, int length) { - throw new UnsupportedOperationException(); + return nioBuffer(index, length); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java index 66c68009a..ba6620cb0 100644 --- a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java +++ b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java @@ -134,6 +134,12 @@ public ByteBuffer[] _nioBuffers(int index, int length) { @Override public ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) { + checkDstIndex(index, length, dstIndex, dst.capacity()); + if (length == 0) { + return this; + } + + // FIXME: check twice here long ri = calculateRelativeIndex(index); index = (int) (ri & Integer.MAX_VALUE); switch ((int) ((ri & MASK) >>> 32L)) { @@ -165,20 +171,22 @@ public ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) { @Override public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) { ByteBuf dstBuf = Unpooled.wrappedBuffer(dst); - int min = Math.min(dst.length, capacity); - return getBytes(0, dstBuf, index, min); + return getBytes(index, dstBuf, dstIndex, length); } @Override public ByteBuf getBytes(int index, ByteBuffer dst) { ByteBuf dstBuf = Unpooled.wrappedBuffer(dst); - int min = Math.min(dst.limit(), capacity); - return getBytes(0, dstBuf, index, min); + return getBytes(index, dstBuf); } @Override public ByteBuf getBytes(int index, final OutputStream out, int length) throws IOException { checkIndex(index, length); + if (length == 0) { + return this; + } + long ri = calculateRelativeIndex(index); index = (int) (ri & Integer.MAX_VALUE); switch ((int) ((ri & MASK) >>> 32L)) { @@ -354,18 +362,12 @@ protected void deallocate() { @Override public String toString(Charset charset) { - StringBuilder builder = new StringBuilder(3); + StringBuilder builder = new StringBuilder(capacity); builder.append(one.toString(charset)); builder.append(two.toString(charset)); return builder.toString(); } - @Override - public String toString(int index, int length, Charset charset) { - // TODO - make this smarter - return toString(charset).substring(index, length); - } - @Override public String toString() { return "Tuple2ByteBuf{" diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java index 1a0c1ec31..be593019f 100644 --- a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java +++ b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java @@ -235,15 +235,13 @@ public ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) { @Override public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) { ByteBuf dstBuf = Unpooled.wrappedBuffer(dst); - int min = Math.min(dst.length, capacity); - return getBytes(0, dstBuf, index, min); + return getBytes(index, dstBuf, dstIndex, length); } @Override public ByteBuf getBytes(int index, ByteBuffer dst) { ByteBuf dstBuf = Unpooled.wrappedBuffer(dst); - int min = Math.min(dst.limit(), capacity); - return getBytes(0, dstBuf, index, min); + return getBytes(index, dstBuf); } @Override @@ -539,12 +537,6 @@ public String toString(Charset charset) { return builder.toString(); } - @Override - public String toString(int index, int length, Charset charset) { - // TODO - make this smarter - return toString(charset).substring(index, length); - } - @Override public String toString() { return "Tuple3ByteBuf{" diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java new file mode 100644 index 000000000..f0f5cf54e --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java @@ -0,0 +1,336 @@ +package io.rsocket.metadata.security; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.rsocket.buffer.TupleByteBuf; +import io.rsocket.util.CharByteBufUtil; + +public class AuthMetadataFlyweight { + + static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 + static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 + + static final int USERNAME_BYTES_LENGTH = 1; + static final int AUTH_TYPE_ID_LENGTH = 1; + + static final char[] EMPTY_CHARS_ARRAY = new char[0]; + + private AuthMetadataFlyweight() {} + + /** + * Encode a Authentication CompositeMetadata payload using custom authentication type + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param customAuthType the custom mime type to encode. + * @param metadata the metadata value to encode. + * @throws IllegalArgumentException in case of {@code customAuthType} is non US_ASCII string or + * empty string or its length is greater than 128 bytes + */ + public static ByteBuf encodeMetadata( + ByteBufAllocator allocator, String customAuthType, ByteBuf metadata) { + + int actualASCIILength = ByteBufUtil.utf8Bytes(customAuthType); + if (actualASCIILength != customAuthType.length()) { + throw new IllegalArgumentException("custom auth type must be US_ASCII characters only"); + } + if (actualASCIILength < 1 || actualASCIILength > 128) { + throw new IllegalArgumentException( + "custom auth type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + int capacity = 1 + actualASCIILength; + ByteBuf headerBuffer = allocator.buffer(capacity, capacity); + // encoded length is one less than actual length, since 0 is never a valid length, which gives + // wider representation range + headerBuffer.writeByte(actualASCIILength - 1); + + ByteBufUtil.reserveAndWriteUtf8(headerBuffer, customAuthType, actualASCIILength); + + return TupleByteBuf.of(allocator, headerBuffer, metadata); + } + + /** + * Encode a Authentication CompositeMetadata payload using custom authentication type + * + * @param allocator the {@link ByteBufAllocator} to create intermediate buffers as needed. + * @param authType the well-known mime type to encode. + * @param metadata the metadata value to encode. + * @throws IllegalArgumentException in case of {@code authType} is {@link + * WellKnownAuthType#UNPARSEABLE_AUTH_TYPE} or {@link + * WellKnownAuthType#UNKNOWN_RESERVED_AUTH_TYPE} + */ + public static ByteBuf encodeMetadata( + ByteBufAllocator allocator, WellKnownAuthType authType, ByteBuf metadata) { + + if (authType == WellKnownAuthType.UNPARSEABLE_AUTH_TYPE + || authType == WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE) { + throw new IllegalArgumentException("only allowed AuthType should be used"); + } + + int capacity = AUTH_TYPE_ID_LENGTH; + ByteBuf headerBuffer = + allocator + .buffer(capacity, capacity) + .writeByte(authType.getIdentifier() | STREAM_METADATA_KNOWN_MASK); + + return TupleByteBuf.of(allocator, headerBuffer, metadata); + } + + /** + * Encode a Authentication CompositeMetadata payload using Simple Authentication format + * + * @throws IllegalArgumentException if the username length is greater than 255 + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param username the char sequence which represents user name. + * @param password the char sequence which represents user password. + */ + public static ByteBuf encodeSimpleMetadata( + ByteBufAllocator allocator, char[] username, char[] password) { + + int usernameLength = CharByteBufUtil.utf8Bytes(username); + if (usernameLength > 255) { + throw new IllegalArgumentException( + "Username should be shorter than or equal to 255 bytes length in UTF-8 encoding"); + } + + int passwordLength = CharByteBufUtil.utf8Bytes(password); + int capacity = AUTH_TYPE_ID_LENGTH + USERNAME_BYTES_LENGTH + usernameLength + passwordLength; + final ByteBuf buffer = + allocator + .buffer(capacity, capacity) + .writeByte(WellKnownAuthType.SIMPLE.getIdentifier() | STREAM_METADATA_KNOWN_MASK) + .writeByte(usernameLength); + + CharByteBufUtil.writeUtf8(buffer, username); + CharByteBufUtil.writeUtf8(buffer, password); + + return buffer; + } + + /** + * Encode a Authentication CompositeMetadata payload using Bearer Authentication format + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param token the char sequence which represents BEARER token. + */ + public static ByteBuf encodeBearerMetadata(ByteBufAllocator allocator, char[] token) { + + int tokenLength = CharByteBufUtil.utf8Bytes(token); + int capacity = AUTH_TYPE_ID_LENGTH + tokenLength; + final ByteBuf buffer = + allocator + .buffer(capacity, capacity) + .writeByte(WellKnownAuthType.BEARER.getIdentifier() | STREAM_METADATA_KNOWN_MASK); + + CharByteBufUtil.writeUtf8(buffer, token); + + return buffer; + } + + /** + * Encode a new Authentication Metadata payload information, first verifying if the passed {@link + * String} matches a {@link WellKnownAuthType} (in which case it will be encoded in a compressed + * fashion using the mime id of that type). + * + *

Prefer using {@link #encodeMetadata(ByteBufAllocator, String, ByteBuf)} if you already know + * that the mime type is not a {@link WellKnownAuthType}. + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param authType the mime type to encode, as a {@link String}. well known mime types are + * compressed. + * @param metadata the metadata value to encode. + * @see #encodeMetadata(ByteBufAllocator, WellKnownAuthType, ByteBuf) + * @see #encodeMetadata(ByteBufAllocator, String, ByteBuf) + */ + public static ByteBuf encodeMetadataWithCompression( + ByteBufAllocator allocator, String authType, ByteBuf metadata) { + WellKnownAuthType wkn = WellKnownAuthType.fromString(authType); + if (wkn == WellKnownAuthType.UNPARSEABLE_AUTH_TYPE) { + return AuthMetadataFlyweight.encodeMetadata(allocator, authType, metadata); + } else { + return AuthMetadataFlyweight.encodeMetadata(allocator, wkn, metadata); + } + } + + /** + * Get the first {@code byte} from a {@link ByteBuf} and check whether it is length or {@link + * WellKnownAuthType}. Assuming said buffer properly contains such a {@code byte} + * + * @param metadata byteBuf used to get information from + */ + public static boolean isWellKnownAuthType(ByteBuf metadata) { + byte lengthOrId = metadata.getByte(0); + return (lengthOrId & STREAM_METADATA_LENGTH_MASK) != lengthOrId; + } + + /** + * Read first byte from the given {@code metadata} and tries to convert it's value to {@link + * WellKnownAuthType}. + * + * @param metadata given metadata buffer to read from + * @return Return on of the know Auth types or {@link WellKnownAuthType#UNPARSEABLE_AUTH_TYPE} if + * field's value is length or unknown auth type + * @throws IllegalStateException if not enough readable bytes in the given {@link ByteBuf} + */ + public static WellKnownAuthType decodeWellKnownAuthType(ByteBuf metadata) { + if (metadata.readableBytes() < 1) { + throw new IllegalStateException( + "Unable to decode Well Know Auth type. Not enough readable bytes"); + } + byte lengthOrId = metadata.readByte(); + int normalizedId = (byte) (lengthOrId & STREAM_METADATA_LENGTH_MASK); + + if (normalizedId != lengthOrId) { + return WellKnownAuthType.fromIdentifier(normalizedId); + } + + return WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; + } + + /** + * Read up to 129 bytes from the given metadata in order to get the custom Auth Type + * + * @param metadata + * @return + */ + public static CharSequence decodeCustomAuthType(ByteBuf metadata) { + if (metadata.readableBytes() < 2) { + throw new IllegalStateException( + "Unable to decode custom Auth type. Not enough readable bytes"); + } + + byte encodedLength = metadata.readByte(); + if (encodedLength < 0) { + throw new IllegalStateException( + "Unable to decode custom Auth type. Incorrect auth type length"); + } + + // encoded length is realLength - 1 in order to avoid intersection with 0x00 authtype + int realLength = encodedLength + 1; + if (metadata.readableBytes() < realLength) { + throw new IllegalArgumentException( + "Unable to decode custom Auth type. Malformed length or auth type string"); + } + + return metadata.readCharSequence(realLength, CharsetUtil.US_ASCII); + } + + /** + * Read all remaining {@code bytes} from the given {@link ByteBuf} and return sliced + * representation of a payload + * + * @param metadata metadata to get payload from. Please note, the {@code metadata#readIndex} + * should be set to the beginning of the payload bytes + * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if no bytes readable in the + * given one + */ + public static ByteBuf decodePayload(ByteBuf metadata) { + if (metadata.readableBytes() == 0) { + return Unpooled.EMPTY_BUFFER; + } + + return metadata.readSlice(metadata.readableBytes()); + } + + /** + * Read up to 257 {@code bytes} from the given {@link ByteBuf} where the first byte is username + * length and the subsequent number of bytes equal to decoded length + * + * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the username length byte + * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if username length is zero + */ + public static ByteBuf decodeUsername(ByteBuf simpleAuthMetadata) { + short usernameLength = decodeUsernameLength(simpleAuthMetadata); + + if (usernameLength == 0) { + return Unpooled.EMPTY_BUFFER; + } + + return simpleAuthMetadata.readSlice(usernameLength); + } + + /** + * Read all the remaining {@code byte}s from the given {@link ByteBuf} which represents user's + * password + * + * @param simpleAuthMetadata the given metadata to read password from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes + * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if password length is zero + */ + public static ByteBuf decodePassword(ByteBuf simpleAuthMetadata) { + if (simpleAuthMetadata.readableBytes() == 0) { + return Unpooled.EMPTY_BUFFER; + } + + return simpleAuthMetadata.readSlice(simpleAuthMetadata.readableBytes()); + } + /** + * Read up to 257 {@code bytes} from the given {@link ByteBuf} where the first byte is username + * length and the subsequent number of bytes equal to decoded length + * + * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the username length byte + * @return {@code char[]} which represents UTF-8 username + */ + public static char[] decodeUsernameAsCharArray(ByteBuf simpleAuthMetadata) { + short usernameLength = decodeUsernameLength(simpleAuthMetadata); + + if (usernameLength == 0) { + return EMPTY_CHARS_ARRAY; + } + + return CharByteBufUtil.readUtf8(simpleAuthMetadata, usernameLength); + } + + /** + * Read all the remaining {@code byte}s from the given {@link ByteBuf} which represents user's + * password + * + * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes + * @return {@code char[]} which represents UTF-8 password + */ + public static char[] decodePasswordAsCharArray(ByteBuf simpleAuthMetadata) { + if (simpleAuthMetadata.readableBytes() == 0) { + return EMPTY_CHARS_ARRAY; + } + + return CharByteBufUtil.readUtf8(simpleAuthMetadata, simpleAuthMetadata.readableBytes()); + } + + /** + * Read all the remaining {@code bytes} from the given {@link ByteBuf} where the first byte is + * username length and the subsequent number of bytes equal to decoded length + * + * @param bearerAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes + * @return {@code char[]} which represents UTF-8 password + */ + public static char[] decodeBearerTokenAsCharArray(ByteBuf bearerAuthMetadata) { + if (bearerAuthMetadata.readableBytes() == 0) { + return EMPTY_CHARS_ARRAY; + } + + return CharByteBufUtil.readUtf8(bearerAuthMetadata, bearerAuthMetadata.readableBytes()); + } + + private static short decodeUsernameLength(ByteBuf simpleAuthMetadata) { + if (simpleAuthMetadata.readableBytes() < 1) { + throw new IllegalStateException( + "Unable to decode custom username. Not enough readable bytes"); + } + + short usernameLength = simpleAuthMetadata.readUnsignedByte(); + + if (simpleAuthMetadata.readableBytes() < usernameLength) { + throw new IllegalArgumentException( + "Unable to decode username. Malformed username length or content"); + } + + return usernameLength; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/security/WellKnownAuthType.java b/rsocket-core/src/main/java/io/rsocket/metadata/security/WellKnownAuthType.java new file mode 100644 index 000000000..bd4b656b8 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/security/WellKnownAuthType.java @@ -0,0 +1,121 @@ +/* + * Copyright 2015-2018 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.metadata.security; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Enumeration of Well Known Auth Types, as defined in the eponymous extension. Such auth types are + * used in composite metadata (which can include routing and/or tracing metadata). Per + * specification, identifiers are between 0 and 127 (inclusive). + */ +public enum WellKnownAuthType { + UNPARSEABLE_AUTH_TYPE("UNPARSEABLE_AUTH_TYPE_DO_NOT_USE", (byte) -2), + UNKNOWN_RESERVED_AUTH_TYPE("UNKNOWN_YET_RESERVED_DO_NOT_USE", (byte) -1), + + SIMPLE("simple", (byte) 0x00), + BEARER("bearer", (byte) 0x01); + // ... reserved for future use ... + + static final WellKnownAuthType[] TYPES_BY_AUTH_ID; + static final Map TYPES_BY_AUTH_STRING; + + static { + // precompute an array of all valid auth ids, filling the blanks with the RESERVED enum + TYPES_BY_AUTH_ID = new WellKnownAuthType[128]; // 0-127 inclusive + Arrays.fill(TYPES_BY_AUTH_ID, UNKNOWN_RESERVED_AUTH_TYPE); + // also prepare a Map of the types by auth string + TYPES_BY_AUTH_STRING = new LinkedHashMap<>(128); + + for (WellKnownAuthType value : values()) { + if (value.getIdentifier() >= 0) { + TYPES_BY_AUTH_ID[value.getIdentifier()] = value; + TYPES_BY_AUTH_STRING.put(value.getString(), value); + } + } + } + + private final byte identifier; + private final String str; + + WellKnownAuthType(String str, byte identifier) { + this.str = str; + this.identifier = identifier; + } + + /** + * Find the {@link WellKnownAuthType} for the given identifier (as an {@code int}). Valid + * identifiers are defined to be integers between 0 and 127, inclusive. Identifiers outside of + * this range will produce the {@link #UNPARSEABLE_AUTH_TYPE}. Additionally, some identifiers in + * that range are still only reserved and don't have a type associated yet: this method returns + * the {@link #UNKNOWN_RESERVED_AUTH_TYPE} when passing such an identifier, which lets call sites + * potentially detect this and keep the original representation when transmitting the associated + * metadata buffer. + * + * @param id the looked up identifier + * @return the {@link WellKnownAuthType}, or {@link #UNKNOWN_RESERVED_AUTH_TYPE} if the id is out + * of the specification's range, or {@link #UNKNOWN_RESERVED_AUTH_TYPE} if the id is one that + * is merely reserved but unknown to this implementation. + */ + public static WellKnownAuthType fromIdentifier(int id) { + if (id < 0x00 || id > 0x7F) { + return UNPARSEABLE_AUTH_TYPE; + } + return TYPES_BY_AUTH_ID[id]; + } + + /** + * Find the {@link WellKnownAuthType} for the given {@link String} representation. If the + * representation is {@code null} or doesn't match a {@link WellKnownAuthType}, the {@link + * #UNPARSEABLE_AUTH_TYPE} is returned. + * + * @param authType the looked up auth type + * @return the matching {@link WellKnownAuthType}, or {@link #UNPARSEABLE_AUTH_TYPE} if none + * matches + */ + public static WellKnownAuthType fromString(String authType) { + if (authType == null) throw new IllegalArgumentException("type must be non-null"); + + // force UNPARSEABLE if by chance UNKNOWN_RESERVED_AUTH_TYPE's text has been used + if (authType.equals(UNKNOWN_RESERVED_AUTH_TYPE.str)) { + return UNPARSEABLE_AUTH_TYPE; + } + + return TYPES_BY_AUTH_STRING.getOrDefault(authType, UNPARSEABLE_AUTH_TYPE); + } + + /** @return the byte identifier of the auth type, guaranteed to be positive or zero. */ + public byte getIdentifier() { + return identifier; + } + + /** + * @return the auth type represented as a {@link String}, which is made of US_ASCII compatible + * characters only + */ + public String getString() { + return str; + } + + /** @see #getString() */ + @Override + public String toString() { + return str; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java b/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java new file mode 100644 index 000000000..e011d2a6f --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java @@ -0,0 +1,208 @@ +package io.rsocket.util; + +import static io.netty.util.internal.StringUtil.isSurrogate; + +import io.netty.buffer.ByteBuf; +import io.netty.util.CharsetUtil; +import io.netty.util.internal.MathUtil; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CoderResult; +import java.util.Arrays; + +public class CharByteBufUtil { + + private static final byte WRITE_UTF_UNKNOWN = (byte) '?'; + + private CharByteBufUtil() {} + + /** + * Returns the exact bytes length of UTF8 character sequence. + * + *

This method is producing the exact length according to {@link #writeUtf8(ByteBuf, char[])}. + */ + public static int utf8Bytes(final char[] seq) { + return utf8ByteCount(seq, 0, seq.length); + } + + /** + * This method is producing the exact length according to {@link #writeUtf8(ByteBuf, char[], int, + * int)}. + */ + public static int utf8Bytes(final char[] seq, int start, int end) { + return utf8ByteCount(checkCharSequenceBounds(seq, start, end), start, end); + } + + private static int utf8ByteCount(final char[] seq, int start, int end) { + int i = start; + // ASCII fast path + while (i < end && seq[i] < 0x80) { + ++i; + } + // !ASCII is packed in a separate method to let the ASCII case be smaller + return i < end ? (i - start) + utf8BytesNonAscii(seq, i, end) : i - start; + } + + private static int utf8BytesNonAscii(final char[] seq, final int start, final int end) { + int encodedLength = 0; + for (int i = start; i < end; i++) { + final char c = seq[i]; + // making it 100% branchless isn't rewarding due to the many bit operations necessary! + if (c < 0x800) { + // branchless version of: (c <= 127 ? 0:1) + 1 + encodedLength += ((0x7f - c) >>> 31) + 1; + } else if (isSurrogate(c)) { + if (!Character.isHighSurrogate(c)) { + encodedLength++; + // WRITE_UTF_UNKNOWN + continue; + } + final char c2; + try { + // Surrogate Pair consumes 2 characters. Optimistically try to get the next character to + // avoid + // duplicate bounds checking with charAt. + c2 = seq[++i]; + } catch (IndexOutOfBoundsException ignored) { + encodedLength++; + // WRITE_UTF_UNKNOWN + break; + } + if (!Character.isLowSurrogate(c2)) { + // WRITE_UTF_UNKNOWN + (Character.isHighSurrogate(c2) ? WRITE_UTF_UNKNOWN : c2) + encodedLength += 2; + continue; + } + // See http://www.unicode.org/versions/Unicode7.0.0/ch03.pdf#G2630. + encodedLength += 4; + } else { + encodedLength += 3; + } + } + return encodedLength; + } + + private static char[] checkCharSequenceBounds(char[] seq, int start, int end) { + if (MathUtil.isOutOfBounds(start, end - start, seq.length)) { + throw new IndexOutOfBoundsException( + "expected: 0 <= start(" + + start + + ") <= end (" + + end + + ") <= seq.length(" + + seq.length + + ')'); + } + return seq; + } + + /** + * Encode a {@link char[]} in UTF-8 and write it + * into {@link ByteBuf}. + * + *

This method returns the actual number of bytes written. + */ + public static int writeUtf8(ByteBuf buf, char[] seq) { + return writeUtf8(buf, seq, 0, seq.length); + } + + /** + * Equivalent to {@link #writeUtf8(ByteBuf, char[]) + * writeUtf8(buf, seq.subSequence(start, end), reserveBytes)} but avoids subsequence object + * allocation if possible. + * + * @return actual number of bytes written + */ + public static int writeUtf8(ByteBuf buf, char[] seq, int start, int end) { + return writeUtf8(buf, buf.writerIndex(), checkCharSequenceBounds(seq, start, end), start, end); + } + + // Fast-Path implementation + static int writeUtf8(ByteBuf buffer, int writerIndex, char[] seq, int start, int end) { + int oldWriterIndex = writerIndex; + + // We can use the _set methods as these not need to do any index checks and reference checks. + // This is possible as we called ensureWritable(...) before. + for (int i = start; i < end; i++) { + char c = seq[i]; + if (c < 0x80) { + buffer.setByte(writerIndex++, (byte) c); + } else if (c < 0x800) { + buffer.setByte(writerIndex++, (byte) (0xc0 | (c >> 6))); + buffer.setByte(writerIndex++, (byte) (0x80 | (c & 0x3f))); + } else if (isSurrogate(c)) { + if (!Character.isHighSurrogate(c)) { + buffer.setByte(writerIndex++, WRITE_UTF_UNKNOWN); + continue; + } + final char c2; + if (seq.length > ++i) { + // Surrogate Pair consumes 2 characters. Optimistically try to get the next character to + // avoid + // duplicate bounds checking with charAt. If an IndexOutOfBoundsException is thrown we + // will + // re-throw a more informative exception describing the problem. + c2 = seq[i]; + } else { + buffer.setByte(writerIndex++, WRITE_UTF_UNKNOWN); + break; + } + // Extra method to allow inlining the rest of writeUtf8 which is the most likely code path. + writerIndex = writeUtf8Surrogate(buffer, writerIndex, c, c2); + } else { + buffer.setByte(writerIndex++, (byte) (0xe0 | (c >> 12))); + buffer.setByte(writerIndex++, (byte) (0x80 | ((c >> 6) & 0x3f))); + buffer.setByte(writerIndex++, (byte) (0x80 | (c & 0x3f))); + } + } + buffer.writerIndex(writerIndex); + return writerIndex - oldWriterIndex; + } + + private static int writeUtf8Surrogate(ByteBuf buffer, int writerIndex, char c, char c2) { + if (!Character.isLowSurrogate(c2)) { + buffer.setByte(writerIndex++, WRITE_UTF_UNKNOWN); + buffer.setByte(writerIndex++, Character.isHighSurrogate(c2) ? WRITE_UTF_UNKNOWN : c2); + return writerIndex; + } + int codePoint = Character.toCodePoint(c, c2); + // See http://www.unicode.org/versions/Unicode7.0.0/ch03.pdf#G2630. + buffer.setByte(writerIndex++, (byte) (0xf0 | (codePoint >> 18))); + buffer.setByte(writerIndex++, (byte) (0x80 | ((codePoint >> 12) & 0x3f))); + buffer.setByte(writerIndex++, (byte) (0x80 | ((codePoint >> 6) & 0x3f))); + buffer.setByte(writerIndex++, (byte) (0x80 | (codePoint & 0x3f))); + return writerIndex; + } + + public static char[] readUtf8(ByteBuf byteBuf, int length) { + CharsetDecoder charsetDecoder = CharsetUtil.UTF_8.newDecoder(); + int en = (int) (length * (double) charsetDecoder.maxCharsPerByte()); + char[] ca = new char[en]; + + CharBuffer charBuffer = CharBuffer.wrap(ca); + ByteBuffer byteBuffer = byteBuf.internalNioBuffer(byteBuf.readerIndex(), length); + byteBuffer.mark(); + try { + CoderResult cr = charsetDecoder.decode(byteBuffer, charBuffer, true); + if (!cr.isUnderflow()) cr.throwException(); + cr = charsetDecoder.flush(charBuffer); + if (!cr.isUnderflow()) cr.throwException(); + + byteBuffer.reset(); + byteBuf.skipBytes(length); + + return safeTrim(charBuffer.array(), charBuffer.position()); + } catch (CharacterCodingException x) { + // Substitution is always enabled, + // so this shouldn't happen + throw new IllegalStateException("unable to decode char array from the given buffer", x); + } + } + + private static char[] safeTrim(char[] ca, int len) { + if (len == ca.length) return ca; + else return Arrays.copyOf(ca, len); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/security/AuthMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/security/AuthMetadataFlyweightTest.java new file mode 100644 index 000000000..13d910e15 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/security/AuthMetadataFlyweightTest.java @@ -0,0 +1,470 @@ +package io.rsocket.metadata.security; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class AuthMetadataFlyweightTest { + + public static final int AUTH_TYPE_ID_LENGTH = 1; + public static final int USER_NAME_BYTES_LENGTH = 1; + public static final String TEST_BEARER_TOKEN = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJpYXQxIjoxNTE2MjM5MDIyLCJpYXQyIjoxNTE2MjM5MDIyLCJpYXQzIjoxNTE2MjM5MDIyLCJpYXQ0IjoxNTE2MjM5MDIyfQ.ljYuH-GNyyhhLcx-rHMchRkGbNsR2_4aSxo8XjrYrSM"; + + @Test + void shouldCorrectlyEncodeData() { + String username = "test"; + String password = "tset1234"; + + int usernameLength = username.length(); + int passwordLength = password.length(); + + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeSimpleMetadata( + ByteBufAllocator.DEFAULT, username.toCharArray(), password.toCharArray()); + + byteBuf.markReaderIndex(); + checkSimpleAuthMetadataEncoding( + username, password, usernameLength, passwordLength, byteBuf.retain()); + byteBuf.resetReaderIndex(); + checkSimpleAuthMetadataEncodingUsingDecoders( + username, password, usernameLength, passwordLength, byteBuf); + } + + @Test + void shouldCorrectlyEncodeData1() { + String username = "𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎"; + String password = "tset1234"; + + int usernameLength = username.getBytes(CharsetUtil.UTF_8).length; + int passwordLength = password.length(); + + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeSimpleMetadata( + ByteBufAllocator.DEFAULT, username.toCharArray(), password.toCharArray()); + + byteBuf.markReaderIndex(); + checkSimpleAuthMetadataEncoding( + username, password, usernameLength, passwordLength, byteBuf.retain()); + byteBuf.resetReaderIndex(); + checkSimpleAuthMetadataEncodingUsingDecoders( + username, password, usernameLength, passwordLength, byteBuf); + } + + @Test + void shouldCorrectlyEncodeData2() { + String username = "𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎1234567#4? "; + String password = "tset1234"; + + int usernameLength = username.getBytes(CharsetUtil.UTF_8).length; + int passwordLength = password.length(); + + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeSimpleMetadata( + ByteBufAllocator.DEFAULT, username.toCharArray(), password.toCharArray()); + + byteBuf.markReaderIndex(); + checkSimpleAuthMetadataEncoding( + username, password, usernameLength, passwordLength, byteBuf.retain()); + byteBuf.resetReaderIndex(); + checkSimpleAuthMetadataEncodingUsingDecoders( + username, password, usernameLength, passwordLength, byteBuf); + } + + private static void checkSimpleAuthMetadataEncoding( + String username, String password, int usernameLength, int passwordLength, ByteBuf byteBuf) { + Assertions.assertThat(byteBuf.capacity()) + .isEqualTo(AUTH_TYPE_ID_LENGTH + USER_NAME_BYTES_LENGTH + usernameLength + passwordLength); + + Assertions.assertThat(byteBuf.readUnsignedByte() & ~0x80) + .isEqualTo(WellKnownAuthType.SIMPLE.getIdentifier()); + Assertions.assertThat(byteBuf.readUnsignedByte()).isEqualTo((short) usernameLength); + + Assertions.assertThat(byteBuf.readCharSequence(usernameLength, CharsetUtil.UTF_8)) + .isEqualTo(username); + Assertions.assertThat(byteBuf.readCharSequence(passwordLength, CharsetUtil.UTF_8)) + .isEqualTo(password); + + ReferenceCountUtil.release(byteBuf); + } + + private static void checkSimpleAuthMetadataEncodingUsingDecoders( + String username, String password, int usernameLength, int passwordLength, ByteBuf byteBuf) { + Assertions.assertThat(byteBuf.capacity()) + .isEqualTo(AUTH_TYPE_ID_LENGTH + USER_NAME_BYTES_LENGTH + usernameLength + passwordLength); + + Assertions.assertThat(AuthMetadataFlyweight.decodeWellKnownAuthType(byteBuf)) + .isEqualTo(WellKnownAuthType.SIMPLE); + byteBuf.markReaderIndex(); + Assertions.assertThat(AuthMetadataFlyweight.decodeUsername(byteBuf).toString(CharsetUtil.UTF_8)) + .isEqualTo(username); + Assertions.assertThat(AuthMetadataFlyweight.decodePassword(byteBuf).toString(CharsetUtil.UTF_8)) + .isEqualTo(password); + byteBuf.resetReaderIndex(); + + Assertions.assertThat(new String(AuthMetadataFlyweight.decodeUsernameAsCharArray(byteBuf))) + .isEqualTo(username); + Assertions.assertThat(new String(AuthMetadataFlyweight.decodePasswordAsCharArray(byteBuf))) + .isEqualTo(password); + + ReferenceCountUtil.release(byteBuf); + } + + @Test + void shouldThrowExceptionIfUsernameLengthExitsAllowedBounds() { + String username = + "𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎𠸏𠹷𠺝𠺢𠻗𠻹𠻺𠼭𠼮𠽌𠾴𠾼𠿪𡁜𡁯𡁵𡁶𡁻𡃁𡃉𡇙𢃇𢞵𢫕𢭃𢯊𢱑𢱕𢳂𢴈𢵌𢵧𢺳𣲷𤓓𤶸𤷪𥄫𦉘𦟌𦧲𦧺𧨾𨅝𨈇𨋢𨳊𨳍𨳒𩶘𠜎𠜱𠝹"; + String password = "tset1234"; + + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.encodeSimpleMetadata( + ByteBufAllocator.DEFAULT, username.toCharArray(), password.toCharArray())) + .hasMessage( + "Username should be shorter than or equal to 255 bytes length in UTF-8 encoding"); + } + + @Test + void shouldEncodeBearerMetadata() { + String testToken = TEST_BEARER_TOKEN; + + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeBearerMetadata( + ByteBufAllocator.DEFAULT, testToken.toCharArray()); + + byteBuf.markReaderIndex(); + checkBearerAuthMetadataEncoding(testToken, byteBuf); + byteBuf.resetReaderIndex(); + checkBearerAuthMetadataEncodingUsingDecoders(testToken, byteBuf); + } + + private static void checkBearerAuthMetadataEncoding(String testToken, ByteBuf byteBuf) { + Assertions.assertThat(byteBuf.capacity()) + .isEqualTo(testToken.getBytes(CharsetUtil.UTF_8).length + AUTH_TYPE_ID_LENGTH); + Assertions.assertThat( + byteBuf.readUnsignedByte() & ~AuthMetadataFlyweight.STREAM_METADATA_KNOWN_MASK) + .isEqualTo(WellKnownAuthType.BEARER.getIdentifier()); + Assertions.assertThat(byteBuf.readSlice(byteBuf.capacity() - 1).toString(CharsetUtil.UTF_8)) + .isEqualTo(testToken); + } + + private static void checkBearerAuthMetadataEncodingUsingDecoders( + String testToken, ByteBuf byteBuf) { + Assertions.assertThat(byteBuf.capacity()) + .isEqualTo(testToken.getBytes(CharsetUtil.UTF_8).length + AUTH_TYPE_ID_LENGTH); + Assertions.assertThat(AuthMetadataFlyweight.isWellKnownAuthType(byteBuf)).isTrue(); + Assertions.assertThat(AuthMetadataFlyweight.decodeWellKnownAuthType(byteBuf)) + .isEqualTo(WellKnownAuthType.BEARER); + byteBuf.markReaderIndex(); + Assertions.assertThat(new String(AuthMetadataFlyweight.decodeBearerTokenAsCharArray(byteBuf))) + .isEqualTo(testToken); + byteBuf.resetReaderIndex(); + Assertions.assertThat( + AuthMetadataFlyweight.decodePayload(byteBuf).toString(CharsetUtil.UTF_8).toString()) + .isEqualTo(testToken); + } + + @Test + void shouldEncodeCustomAuth() { + String payloadAsAText = "testsecuritybuffer"; + ByteBuf testSecurityPayload = + Unpooled.wrappedBuffer(payloadAsAText.getBytes(CharsetUtil.UTF_8)); + + String customAuthType = "myownauthtype"; + ByteBuf buffer = + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, customAuthType, testSecurityPayload); + + checkCustomAuthMetadataEncoding(testSecurityPayload, customAuthType, buffer); + } + + private static void checkCustomAuthMetadataEncoding( + ByteBuf testSecurityPayload, String customAuthType, ByteBuf buffer) { + Assertions.assertThat(buffer.capacity()) + .isEqualTo(1 + customAuthType.length() + testSecurityPayload.capacity()); + Assertions.assertThat(buffer.readUnsignedByte()) + .isEqualTo((short) (customAuthType.length() - 1)); + Assertions.assertThat( + buffer.readCharSequence(customAuthType.length(), CharsetUtil.US_ASCII).toString()) + .isEqualTo(customAuthType); + Assertions.assertThat(buffer.readSlice(testSecurityPayload.capacity())) + .isEqualTo(testSecurityPayload); + + ReferenceCountUtil.release(buffer); + } + + @Test + void shouldThrowOnNonASCIIChars() { + ByteBuf testSecurityPayload = ByteBufAllocator.DEFAULT.buffer(); + String customAuthType = "1234567#4? 𠜎𠜱𠝹𠱓𠱸𠲖𠳏𠳕𠴕𠵼𠵿𠸎"; + + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, customAuthType, testSecurityPayload)) + .hasMessage("custom auth type must be US_ASCII characters only"); + } + + @Test + void shouldThrowOnOutOfAllowedSizeType() { + ByteBuf testSecurityPayload = ByteBufAllocator.DEFAULT.buffer(); + // 130 chars + String customAuthType = + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"; + + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, customAuthType, testSecurityPayload)) + .hasMessage( + "custom auth type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + @Test + void shouldThrowOnOutOfAllowedSizeType1() { + ByteBuf testSecurityPayload = ByteBufAllocator.DEFAULT.buffer(); + String customAuthType = ""; + + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, customAuthType, testSecurityPayload)) + .hasMessage( + "custom auth type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + @Test + void shouldEncodeUsingWellKnownAuthType() { + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, + WellKnownAuthType.SIMPLE, + ByteBufAllocator.DEFAULT.buffer(3, 3).writeByte(1).writeByte('u').writeByte('p')); + + checkSimpleAuthMetadataEncoding("u", "p", 1, 1, byteBuf); + } + + @Test + void shouldEncodeUsingWellKnownAuthType1() { + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, + WellKnownAuthType.SIMPLE, + ByteBufAllocator.DEFAULT.buffer().writeByte(1).writeByte('u').writeByte('p')); + + checkSimpleAuthMetadataEncoding("u", "p", 1, 1, byteBuf); + } + + @Test + void shouldEncodeUsingWellKnownAuthType2() { + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, + WellKnownAuthType.BEARER, + Unpooled.copiedBuffer(TEST_BEARER_TOKEN, CharsetUtil.UTF_8)); + + byteBuf.markReaderIndex(); + checkBearerAuthMetadataEncoding(TEST_BEARER_TOKEN, byteBuf); + byteBuf.resetReaderIndex(); + checkBearerAuthMetadataEncodingUsingDecoders(TEST_BEARER_TOKEN, byteBuf); + } + + @Test + void shouldThrowIfWellKnownAuthTypeIsUnsupportedOrUnknown() { + ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer(); + + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, WellKnownAuthType.UNPARSEABLE_AUTH_TYPE, buffer)) + .hasMessage("only allowed AuthType should be used"); + + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, WellKnownAuthType.UNPARSEABLE_AUTH_TYPE, buffer)) + .hasMessage("only allowed AuthType should be used"); + + buffer.release(); + } + + @Test + void shouldCompressMetadata() { + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, + "simple", + ByteBufAllocator.DEFAULT.buffer().writeByte(1).writeByte('u').writeByte('p')); + + checkSimpleAuthMetadataEncoding("u", "p", 1, 1, byteBuf); + } + + @Test + void shouldCompressMetadata1() { + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, + "bearer", + Unpooled.copiedBuffer(TEST_BEARER_TOKEN, CharsetUtil.UTF_8)); + + byteBuf.markReaderIndex(); + checkBearerAuthMetadataEncoding(TEST_BEARER_TOKEN, byteBuf); + byteBuf.resetReaderIndex(); + checkBearerAuthMetadataEncodingUsingDecoders(TEST_BEARER_TOKEN, byteBuf); + } + + @Test + void shouldNotCompressMetadata() { + ByteBuf testMetadataPayload = + Unpooled.wrappedBuffer(TEST_BEARER_TOKEN.getBytes(CharsetUtil.UTF_8)); + String customAuthType = "testauthtype"; + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, customAuthType, testMetadataPayload); + + checkCustomAuthMetadataEncoding(testMetadataPayload, customAuthType, byteBuf); + } + + @Test + void shouldConfirmWellKnownAuthType() { + ByteBuf metadata = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, "simple", Unpooled.EMPTY_BUFFER); + + int initialReaderIndex = metadata.readerIndex(); + + Assertions.assertThat(AuthMetadataFlyweight.isWellKnownAuthType(metadata)).isTrue(); + Assertions.assertThat(metadata.readerIndex()).isEqualTo(initialReaderIndex); + + ReferenceCountUtil.release(metadata); + } + + @Test + void shouldConfirmGivenMetadataIsNotAWellKnownAuthType() { + ByteBuf metadata = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, "simple/afafgafadf", Unpooled.EMPTY_BUFFER); + + int initialReaderIndex = metadata.readerIndex(); + + Assertions.assertThat(AuthMetadataFlyweight.isWellKnownAuthType(metadata)).isFalse(); + Assertions.assertThat(metadata.readerIndex()).isEqualTo(initialReaderIndex); + + ReferenceCountUtil.release(metadata); + } + + @Test + void shouldReadSimpleWellKnownAuthType() { + ByteBuf metadata = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, "simple", Unpooled.EMPTY_BUFFER); + WellKnownAuthType expectedType = WellKnownAuthType.SIMPLE; + checkDecodeWellKnowAuthTypeCorrectly(metadata, expectedType); + } + + @Test + void shouldReadSimpleWellKnownAuthType1() { + ByteBuf metadata = + AuthMetadataFlyweight.encodeMetadataWithCompression( + ByteBufAllocator.DEFAULT, "bearer", Unpooled.EMPTY_BUFFER); + WellKnownAuthType expectedType = WellKnownAuthType.BEARER; + checkDecodeWellKnowAuthTypeCorrectly(metadata, expectedType); + } + + @Test + void shouldReadSimpleWellKnownAuthType2() { + ByteBuf metadata = + ByteBufAllocator.DEFAULT + .buffer() + .writeByte(3 | AuthMetadataFlyweight.STREAM_METADATA_KNOWN_MASK); + WellKnownAuthType expectedType = WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE; + checkDecodeWellKnowAuthTypeCorrectly(metadata, expectedType); + } + + @Test + void shouldNotReadSimpleWellKnownAuthTypeIfEncodedLength() { + ByteBuf metadata = ByteBufAllocator.DEFAULT.buffer().writeByte(3); + WellKnownAuthType expectedType = WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; + checkDecodeWellKnowAuthTypeCorrectly(metadata, expectedType); + } + + @Test + void shouldNotReadSimpleWellKnownAuthTypeIfEncodedLength1() { + ByteBuf metadata = + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, "testmetadataauthtype", Unpooled.EMPTY_BUFFER); + WellKnownAuthType expectedType = WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; + checkDecodeWellKnowAuthTypeCorrectly(metadata, expectedType); + } + + @Test + void shouldThrowExceptionIsNotEnoughReadableBytes() { + Assertions.assertThatThrownBy( + () -> AuthMetadataFlyweight.decodeWellKnownAuthType(Unpooled.EMPTY_BUFFER)) + .hasMessage("Unable to decode Well Know Auth type. Not enough readable bytes"); + } + + private static void checkDecodeWellKnowAuthTypeCorrectly( + ByteBuf metadata, WellKnownAuthType expectedType) { + int initialReaderIndex = metadata.readerIndex(); + + WellKnownAuthType wellKnownAuthType = AuthMetadataFlyweight.decodeWellKnownAuthType(metadata); + + Assertions.assertThat(wellKnownAuthType).isEqualTo(expectedType); + Assertions.assertThat(metadata.readerIndex()) + .isNotEqualTo(initialReaderIndex) + .isEqualTo(initialReaderIndex + 1); + + ReferenceCountUtil.release(metadata); + } + + @Test + void shouldReadCustomEncodedAuthType() { + String testAuthType = "TestAuthType"; + ByteBuf byteBuf = + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, testAuthType, Unpooled.EMPTY_BUFFER); + checkDecodeCustomAuthTypeCorrectly(testAuthType, byteBuf); + } + + @Test + void shouldThrowExceptionOnEmptyMetadata() { + Assertions.assertThatThrownBy( + () -> AuthMetadataFlyweight.decodeCustomAuthType(Unpooled.EMPTY_BUFFER)) + .hasMessage("Unable to decode custom Auth type. Not enough readable bytes"); + } + + @Test + void shouldThrowExceptionOnMalformedMetadata_wellknowninstead() { + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.decodeCustomAuthType( + AuthMetadataFlyweight.encodeMetadata( + ByteBufAllocator.DEFAULT, + WellKnownAuthType.BEARER, + Unpooled.copiedBuffer(new byte[] {'a', 'b'})))) + .hasMessage("Unable to decode custom Auth type. Incorrect auth type length"); + } + + @Test + void shouldThrowExceptionOnMalformedMetadata_length() { + Assertions.assertThatThrownBy( + () -> + AuthMetadataFlyweight.decodeCustomAuthType( + ByteBufAllocator.DEFAULT.buffer().writeByte(127).writeChar('a').writeChar('b'))) + .hasMessage("Unable to decode custom Auth type. Malformed length or auth type string"); + } + + private static void checkDecodeCustomAuthTypeCorrectly(String testAuthType, ByteBuf byteBuf) { + int initialReaderIndex = byteBuf.readerIndex(); + + Assertions.assertThat(AuthMetadataFlyweight.decodeCustomAuthType(byteBuf).toString()) + .isEqualTo(testAuthType); + Assertions.assertThat(byteBuf.readerIndex()) + .isEqualTo(initialReaderIndex + testAuthType.length() + 1); + } +} From 3f5b30448777b631af0de96aff9639096639a0fc Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 4 Feb 2020 18:37:33 +0100 Subject: [PATCH 121/181] bumps version Signed-off-by: Oleh Dokuka --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 13a89e30c..3018f4792 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-RC6 -perfBaselineVersion=1.0.0-RC5 +version=1.0.0-RC7 +perfBaselineVersion=1.0.0-RC6 From f689f54913f09484aaf2038936faa62333b73dd5 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sat, 22 Feb 2020 22:31:41 +0200 Subject: [PATCH 122/181] Provides Prioritised delivering of Zero Streams Frames (#718) * prioritization Signed-off-by: Oleh Dokuka * provides prioritized sending for zero-stream frames; refactors benchmarks Signed-off-by: Oleh Dokuka * provides minor cleanups Signed-off-by: Oleh Dokuka Signed-off-by: Oleh Dokuka * more fixes Signed-off-by: Oleh Dokuka * fixes format Signed-off-by: Oleh Dokuka * fixes Signed-off-by: Oleh Dokuka --- build.gradle | 5 +- .../java/io/rsocket/RSocketRequester.java | 4 +- .../java/io/rsocket/RSocketResponder.java | 2 +- .../rsocket/internal/UnboundedProcessor.java | 70 +++++++++++++------ .../io/rsocket/internal/UnicastMonoEmpty.java | 6 +- .../jctools/queues/SupportsIterator.java | 20 ------ .../internal/UnboundedProcessorTest.java | 31 ++++++++ 7 files changed, 88 insertions(+), 50 deletions(-) delete mode 100644 rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/SupportsIterator.java diff --git a/build.gradle b/build.gradle index e834e21bd..7f7a3dea2 100644 --- a/build.gradle +++ b/build.gradle @@ -88,8 +88,11 @@ subprojects { repositories { mavenCentral() - if (version.endsWith('BUILD-SNAPSHOT') || project.hasProperty('platformVersion')) { + if (version.endsWith('SNAPSHOT') || project.hasProperty('platformVersion')) { maven { url 'http://repo.spring.io/libs-snapshot' } + maven { + url 'https://oss.jfrog.org/artifactory/oss-snapshot-local' + } } } diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java index 5590a9df0..ed13e9f6e 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java @@ -124,7 +124,7 @@ class RSocketRequester implements RSocket { new ClientKeepAliveSupport(allocator, keepAliveTickPeriod, keepAliveAckTimeout); this.keepAliveFramesAcceptor = keepAliveHandler.start( - keepAliveSupport, sendProcessor::onNext, this::tryTerminateOnKeepAlive); + keepAliveSupport, sendProcessor::onNextPrioritized, this::tryTerminateOnKeepAlive); } else { keepAliveFramesAcceptor = null; } @@ -411,7 +411,7 @@ private Mono handleMetadataPush(Payload payload) { MetadataPushFrameFlyweight.encode(allocator, payload.sliceMetadata().retain()); payload.release(); - sendProcessor.onNext(metadataPushFrame); + sendProcessor.onNextPrioritized(metadataPushFrame); }); } diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java index 490b00967..53ced9763 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java @@ -85,7 +85,7 @@ class RSocketResponder implements ResponderRSocket { .subscribe(null, this::handleSendProcessorError); Disposable receiveDisposable = connection.receive().subscribe(this::handleFrame, errorConsumer); - Disposable sendLeaseDisposable = leaseHandler.send(sendProcessor::onNext); + Disposable sendLeaseDisposable = leaseHandler.send(sendProcessor::onNextPrioritized); this.connection .onClose() 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 dfcc13a64..fe664e843 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -56,6 +56,7 @@ public final class UnboundedProcessor extends FluxProcessor AtomicLongFieldUpdater.newUpdater(UnboundedProcessor.class, "requested"); final Queue queue; + final Queue priorityQueue; volatile boolean done; Throwable error; volatile CoreSubscriber actual; @@ -67,11 +68,12 @@ public final class UnboundedProcessor extends FluxProcessor public UnboundedProcessor() { this.queue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); + this.priorityQueue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); } @Override public int getBufferSize() { - return Queues.capacity(this.queue); + return Integer.MAX_VALUE; } @Override @@ -84,6 +86,7 @@ void drainRegular(Subscriber a) { int missed = 1; final Queue q = queue; + final Queue pq = priorityQueue; for (; ; ) { @@ -93,10 +96,18 @@ void drainRegular(Subscriber a) { while (r != e) { boolean d = done; - T t = q.poll(); - boolean empty = t == null; + T t; + boolean empty; + + if (!pq.isEmpty()) { + t = pq.poll(); + empty = false; + } else { + t = q.poll(); + empty = t == null; + } - if (checkTerminated(d, empty, a, q)) { + if (checkTerminated(d, empty, a)) { return; } @@ -110,7 +121,7 @@ void drainRegular(Subscriber a) { } if (r == e) { - if (checkTerminated(done, q.isEmpty(), a, q)) { + if (checkTerminated(done, q.isEmpty() && pq.isEmpty(), a)) { return; } } @@ -129,12 +140,10 @@ void drainRegular(Subscriber a) { void drainFused(Subscriber a) { int missed = 1; - final Queue q = queue; - for (; ; ) { if (cancelled) { - q.clear(); + clear(); actual = null; return; } @@ -188,14 +197,9 @@ public void drain() { } } - boolean checkTerminated(boolean d, boolean empty, Subscriber a, Queue q) { + boolean checkTerminated(boolean d, boolean empty, Subscriber a) { if (cancelled) { - while (!q.isEmpty()) { - T t = q.poll(); - if (t != null) { - release(t); - } - } + clear(); actual = null; return true; } @@ -237,6 +241,23 @@ public Context currentContext() { return actual != null ? actual.currentContext() : Context.empty(); } + public void onNextPrioritized(T t) { + if (done || cancelled) { + Operators.onNextDropped(t, currentContext()); + release(t); + return; + } + + if (!priorityQueue.offer(t)) { + Throwable ex = + Operators.onOperatorError(null, Exceptions.failWithOverflow(), t, currentContext()); + onError(Operators.onOperatorError(null, ex, t, currentContext())); + release(t); + return; + } + drain(); + } + @Override public void onNext(T t) { if (done || cancelled) { @@ -319,25 +340,24 @@ public void cancel() { } } - @Override - public T peek() { - return queue.peek(); - } - @Override @Nullable public T poll() { + Queue pq = this.priorityQueue; + if (!pq.isEmpty()) { + return pq.poll(); + } return queue.poll(); } @Override public int size() { - return queue.size(); + return priorityQueue.size() + queue.size(); } @Override public boolean isEmpty() { - return queue.isEmpty(); + return priorityQueue.isEmpty() && queue.isEmpty(); } @Override @@ -348,6 +368,12 @@ public void clear() { release(t); } } + while (!priorityQueue.isEmpty()) { + T t = priorityQueue.poll(); + if (t != null) { + release(t); + } + } } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoEmpty.java b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoEmpty.java index eb8a1aa11..64a7d4422 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoEmpty.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoEmpty.java @@ -9,10 +9,8 @@ import reactor.util.annotation.Nullable; /** - * Represents an empty publisher which only calls onSubscribe and onComplete. - * - *

This Publisher is effectively stateless and only a single instance exists. Use the {@link - * #instance()} method to obtain a properly type-parametrized view of it. + * Represents an empty publisher which only calls onSubscribe and onComplete and allows only a + * single subscriber. * * @see Reactive-Streams-Commons */ diff --git a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/SupportsIterator.java b/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/SupportsIterator.java deleted file mode 100644 index 50d2a326f..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/jctools/queues/SupportsIterator.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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.internal.jctools.queues; - -import io.rsocket.internal.jctools.util.InternalAPI; - -/** Tagging interface to help testing */ -@InternalAPI -public interface SupportsIterator {} diff --git a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java index 0dc7d9090..7bf975543 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/UnboundedProcessorTest.java @@ -17,6 +17,7 @@ package io.rsocket.internal; import io.rsocket.Payload; +import io.rsocket.util.ByteBufPayload; import io.rsocket.util.EmptyPayload; import java.util.concurrent.CountDownLatch; import org.junit.Assert; @@ -82,6 +83,36 @@ public void testOnNextAfterSubscribe_1000() throws Exception { testOnNextAfterSubscribeN(1000); } + @Test + public void testPrioritizedSending() { + UnboundedProcessor processor = new UnboundedProcessor<>(); + + for (int i = 0; i < 1000; i++) { + processor.onNext(EmptyPayload.INSTANCE); + } + + processor.onNextPrioritized(ByteBufPayload.create("test")); + + Payload closestPayload = processor.next().block(); + + Assert.assertEquals(closestPayload.getDataUtf8(), "test"); + } + + @Test + public void testPrioritizedFused() { + UnboundedProcessor processor = new UnboundedProcessor<>(); + + for (int i = 0; i < 1000; i++) { + processor.onNext(EmptyPayload.INSTANCE); + } + + processor.onNextPrioritized(ByteBufPayload.create("test")); + + Payload closestPayload = processor.poll(); + + Assert.assertEquals(closestPayload.getDataUtf8(), "test"); + } + public void testOnNextAfterSubscribeN(int n) throws Exception { CountDownLatch latch = new CountDownLatch(n); UnboundedProcessor processor = new UnboundedProcessor<>(); From c66dfb8b6d9d1a51655cde43dcf56666fa251162 Mon Sep 17 00:00:00 2001 From: Jacky Chan Date: Tue, 25 Feb 2020 03:02:21 -0800 Subject: [PATCH 123/181] add MESSAGE_RSOCKET_MIMETYPE and MESSAGE_RSOCKET_ACCEPT_MIMETYPES (#744) Signed-off-by: linux_china --- .../src/main/java/io/rsocket/metadata/WellKnownMimeType.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java index 2743f604d..e78e87629 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownMimeType.java @@ -72,7 +72,8 @@ public enum WellKnownMimeType { APPLICATION_CLOUDEVENTS_JSON("application/cloudevents+json", (byte) 0x28), // ... reserved for future use ... - + MESSAGE_RSOCKET_MIMETYPE("message/x.rsocket.mime-type.v0", (byte) 0x7A), + MESSAGE_RSOCKET_ACCEPT_MIMETYPES("message/x.rsocket.accept-mime-types.v0", (byte) 0x7B), MESSAGE_RSOCKET_AUTHENTICATION("message/x.rsocket.authentication.v0", (byte) 0x7C), MESSAGE_RSOCKET_TRACING_ZIPKIN("message/x.rsocket.tracing-zipkin.v0", (byte) 0x7D), MESSAGE_RSOCKET_ROUTING("message/x.rsocket.routing.v0", (byte) 0x7E), From 2f71f730c39eec4a05ac01cd679d0fb341cf0a6a Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 8 Mar 2020 21:41:40 +0200 Subject: [PATCH 124/181] disables gradle modules feature (#751) Signed-off-by: Oleh Dokuka --- build.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 7f7a3dea2..80b16619d 100644 --- a/build.gradle +++ b/build.gradle @@ -96,6 +96,10 @@ subprojects { } } + tasks.withType(GenerateModuleMetadata) { + enabled = false + } + plugins.withType(JavaPlugin) { compileJava { sourceCompatibility = 1.8 From 0cc46d0e23b2d6746efdfc7d25f64f98fb3bea77 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 22 Mar 2020 15:28:35 +0200 Subject: [PATCH 125/181] provides rollback to CompositeByteBuf usage (#750) * provides rollback to CompositeByteBuf usage Signed-off-by: Oleh Dokuka * fixes test Signed-off-by: Oleh Dokuka --- .../frame/DataAndMetadataFlyweight.java | 7 +- .../rsocket/frame/FrameLengthFlyweight.java | 3 +- .../security/AuthMetadataFlyweight.java | 5 +- .../java/io/rsocket/util/CharByteBufUtil.java | 5 +- .../java/io/rsocket/test/TransportTest.java | 76 +++++++++++++++++- .../main/resources/words.shakespeare.txt.gz | Bin 0 -> 81824 bytes .../netty/TcpSecureTransportTest.java | 55 +++++++++++++ 7 files changed, 138 insertions(+), 13 deletions(-) create mode 100644 rsocket-test/src/main/resources/words.shakespeare.txt.gz create mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java index e4b16fec7..d910fe92f 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java @@ -3,7 +3,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; -import io.rsocket.buffer.TupleByteBuf; class DataAndMetadataFlyweight { public static final int FRAME_LENGTH_MASK = 0xFFFFFF; @@ -33,11 +32,11 @@ private static int decodeLength(final ByteBuf byteBuf) { static ByteBuf encodeOnlyMetadata( ByteBufAllocator allocator, final ByteBuf header, ByteBuf metadata) { - return TupleByteBuf.of(allocator, header, metadata); + return allocator.compositeBuffer(2).addComponents(true, header, metadata); } static ByteBuf encodeOnlyData(ByteBufAllocator allocator, final ByteBuf header, ByteBuf data) { - return TupleByteBuf.of(allocator, header, data); + return allocator.compositeBuffer(2).addComponents(true, header, data); } static ByteBuf encode( @@ -45,7 +44,7 @@ static ByteBuf encode( int length = metadata.readableBytes(); encodeLength(header, length); - return TupleByteBuf.of(allocator, header, metadata, data); + return allocator.compositeBuffer(3).addComponents(true, header, metadata, data); } static ByteBuf metadataWithoutMarking(ByteBuf byteBuf, boolean hasMetadata) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java index 6011263fa..622160061 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java @@ -2,7 +2,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.rsocket.buffer.TupleByteBuf; /** * Some transports like TCP aren't framed, and require a length. This is used by DuplexConnections @@ -35,7 +34,7 @@ private static int decodeLength(final ByteBuf byteBuf) { public static ByteBuf encode(ByteBufAllocator allocator, int length, ByteBuf frame) { ByteBuf buffer = allocator.buffer(); encodeLength(buffer, length); - return TupleByteBuf.of(allocator, buffer, frame); + return allocator.compositeBuffer(2).addComponents(true, buffer, frame); } public static int length(ByteBuf byteBuf) { diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java index f0f5cf54e..27bf4d1da 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java @@ -5,7 +5,6 @@ import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; -import io.rsocket.buffer.TupleByteBuf; import io.rsocket.util.CharByteBufUtil; public class AuthMetadataFlyweight { @@ -49,7 +48,7 @@ public static ByteBuf encodeMetadata( ByteBufUtil.reserveAndWriteUtf8(headerBuffer, customAuthType, actualASCIILength); - return TupleByteBuf.of(allocator, headerBuffer, metadata); + return allocator.compositeBuffer(2).addComponents(true, headerBuffer, metadata); } /** @@ -76,7 +75,7 @@ public static ByteBuf encodeMetadata( .buffer(capacity, capacity) .writeByte(authType.getIdentifier() | STREAM_METADATA_KNOWN_MASK); - return TupleByteBuf.of(allocator, headerBuffer, metadata); + return allocator.compositeBuffer(2).addComponents(true, headerBuffer, metadata); } /** diff --git a/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java b/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java index e011d2a6f..6f2aa7150 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java @@ -182,7 +182,10 @@ public static char[] readUtf8(ByteBuf byteBuf, int length) { char[] ca = new char[en]; CharBuffer charBuffer = CharBuffer.wrap(ca); - ByteBuffer byteBuffer = byteBuf.internalNioBuffer(byteBuf.readerIndex(), length); + ByteBuffer byteBuffer = + byteBuf.nioBufferCount() == 1 + ? byteBuf.internalNioBuffer(byteBuf.readerIndex(), length) + : byteBuf.nioBuffer(byteBuf.readerIndex(), length); byteBuffer.mark(); try { CoderResult cr = charsetDecoder.decode(byteBuffer, charBuffer, true); diff --git a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java index fc6301d7d..6721acf2c 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -23,10 +23,14 @@ import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.util.DefaultPayload; +import java.io.BufferedReader; +import java.io.InputStreamReader; import java.time.Duration; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.zip.GZIPInputStream; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -38,6 +42,25 @@ public interface TransportTest { + String MOCK_DATA = "test-data"; + String MOCK_METADATA = "metadata"; + String LARGE_DATA = read("words.shakespeare.txt.gz"); + Payload LARGE_PAYLOAD = DefaultPayload.create(LARGE_DATA, LARGE_DATA); + + static String read(String resourceName) { + + try (BufferedReader br = + new BufferedReader( + new InputStreamReader( + new GZIPInputStream( + TransportTest.class.getClassLoader().getResourceAsStream(resourceName))))) { + + return br.lines().map(String::toLowerCase).collect(Collectors.joining("\n\r")); + } catch (Throwable e) { + throw new RuntimeException(e); + } + } + @AfterEach default void close() { getTransportPair().dispose(); @@ -54,12 +77,12 @@ default Payload createTestPayload(int metadataPresent) { metadata1 = ""; break; default: - metadata1 = "metadata"; + metadata1 = MOCK_METADATA; break; } String metadata = metadata1; - return DefaultPayload.create("test-data", metadata); + return DefaultPayload.create(MOCK_DATA, metadata); } @DisplayName("makes 10 fireAndForget requests") @@ -73,6 +96,17 @@ default void fireAndForget10() { .verify(getTimeout()); } + @DisplayName("makes 10 fireAndForget with Large Payload in Requests") + @Test + default void largePayloadFireAndForget10() { + Flux.range(1, 10) + .flatMap(i -> getClient().fireAndForget(LARGE_PAYLOAD)) + .as(StepVerifier::create) + .expectNextCount(0) + .expectComplete() + .verify(getTimeout()); + } + default RSocket getClient() { return getTransportPair().getClient(); } @@ -92,6 +126,17 @@ default void metadataPush10() { .verify(getTimeout()); } + @DisplayName("makes 10 metadataPush with Large Metadata in requests") + @Test + default void largePayloadMetadataPush10() { + Flux.range(1, 10) + .flatMap(i -> getClient().metadataPush(DefaultPayload.create("", LARGE_DATA))) + .as(StepVerifier::create) + .expectNextCount(0) + .expectComplete() + .verify(getTimeout()); + } + @DisplayName("makes 1 requestChannel request with 0 payloads") @Test default void requestChannel0() { @@ -127,6 +172,19 @@ default void requestChannel200_000() { .verify(getTimeout()); } + @DisplayName("makes 1 requestChannel request with 2,000 large payloads") + @Test + default void largePayloadRequestChannel200() { + Flux payloads = Flux.range(0, 200).map(__ -> LARGE_PAYLOAD); + + getClient() + .requestChannel(payloads) + .as(StepVerifier::create) + .expectNextCount(200) + .expectComplete() + .verify(getTimeout()); + } + @DisplayName("makes 1 requestChannel request with 20,000 payloads") @Test default void requestChannel20_000() { @@ -223,6 +281,17 @@ default void requestResponse100() { .verify(getTimeout()); } + @DisplayName("makes 100 requestResponse requests") + @Test + default void largePayloadRequestResponse100() { + Flux.range(1, 100) + .flatMap(i -> getClient().requestResponse(LARGE_PAYLOAD).map(Payload::getDataUtf8)) + .as(StepVerifier::create) + .expectNextCount(100) + .expectComplete() + .verify(getTimeout()); + } + @DisplayName("makes 10,000 requestResponse requests") @Test default void requestResponse10_000() { @@ -283,7 +352,7 @@ default void assertPayload(Payload p) { } default void assertChannelPayload(Payload p) { - if (!"test-data".equals(p.getDataUtf8()) || !"metadata".equals(p.getMetadataUtf8())) { + if (!MOCK_DATA.equals(p.getDataUtf8()) || !MOCK_METADATA.equals(p.getMetadataUtf8())) { throw new IllegalStateException("Unexpected payload"); } } @@ -312,6 +381,7 @@ public TransportPair( client = RSocketFactory.connect() + .keepAlive(Duration.ZERO, Duration.ZERO, 1) .transport(clientTransportSupplier.apply(address, server)) .start() .doOnError(Throwable::printStackTrace) diff --git a/rsocket-test/src/main/resources/words.shakespeare.txt.gz b/rsocket-test/src/main/resources/words.shakespeare.txt.gz new file mode 100644 index 0000000000000000000000000000000000000000..422a4b331007ff9d1f575ebcaf12e1da47d1bb94 GIT binary patch literal 81824 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 r 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 Gj{^We%n*YB literal 0 HcmV?d00001 diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java new file mode 100644 index 000000000..b77de6d4e --- /dev/null +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java @@ -0,0 +1,55 @@ +package io.rsocket.transport.netty; + +import io.netty.handler.ssl.SslContextBuilder; +import io.netty.handler.ssl.util.InsecureTrustManagerFactory; +import io.netty.handler.ssl.util.SelfSignedCertificate; +import io.rsocket.test.TransportTest; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; +import java.net.InetSocketAddress; +import java.security.cert.CertificateException; +import java.time.Duration; +import reactor.core.Exceptions; +import reactor.netty.tcp.TcpClient; +import reactor.netty.tcp.TcpServer; + +public class TcpSecureTransportTest implements TransportTest { + private final TransportPair transportPair = + new TransportPair<>( + () -> new InetSocketAddress("localhost", 0), + (address, server) -> + TcpClientTransport.create( + TcpClient.create() + .addressSupplier(server::address) + .secure( + ssl -> + ssl.sslContext( + SslContextBuilder.forClient() + .trustManager(InsecureTrustManagerFactory.INSTANCE)))), + address -> { + try { + SelfSignedCertificate ssc = new SelfSignedCertificate(); + TcpServer server = + TcpServer.create() + .addressSupplier(() -> address) + .secure( + ssl -> + ssl.sslContext( + SslContextBuilder.forServer( + ssc.certificate(), ssc.privateKey()))); + return TcpServerTransport.create(server); + } catch (CertificateException e) { + throw Exceptions.propagate(e); + } + }); + + @Override + public Duration getTimeout() { + return Duration.ofMinutes(10); + } + + @Override + public TransportPair getTransportPair() { + return transportPair; + } +} From d1c36ba7d9e61c3d9d3c5d79b03e9a4ba424ffee Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 24 Mar 2020 14:25:05 +0200 Subject: [PATCH 126/181] updates to latest reactor / netty versions Signed-off-by: Oleh Dokuka --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 80b16619d..262015af6 100644 --- a/build.gradle +++ b/build.gradle @@ -27,11 +27,11 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = 'Dysprosium-RELEASE' + ext['reactor-bom.version'] = 'Dysprosium-SR6' ext['logback.version'] = '1.2.3' ext['findbugs.version'] = '3.0.2' - ext['netty-bom.version'] = '4.1.37.Final' - ext['netty-boringssl.version'] = '2.0.25.Final' + ext['netty-bom.version'] = '4.1.48.Final' + ext['netty-boringssl.version'] = '2.0.30.Final' ext['hdrhistogram.version'] = '2.1.10' ext['mockito.version'] = '3.2.0' ext['slf4j.version'] = '1.7.25' From da18ea1e0797032bc6e29dcf58fe5c5d60ecc595 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 25 Mar 2020 13:47:54 +0200 Subject: [PATCH 127/181] Reconnectable and Shared Mono feature (#759) * provides a reconnectable Mono feature Signed-off-by: Oleh Dokuka * fixes interface naming Signed-off-by: Oleh Dokuka * makes usage of retry only in case whenFactory is set up Signed-off-by: Oleh Dokuka * refactors to convenient Retry Signed-off-by: Oleh Dokuka * removes noargs reconnect() method Signed-off-by: Oleh Dokuka * fixes tests Signed-off-by: Oleh Dokuka * Update rsocket-core/src/main/java/io/rsocket/RSocketFactory.java Co-Authored-By: Rossen Stoyanchev * fixes docs Signed-off-by: Oleh Dokuka * provide test events logging Signed-off-by: Oleh Dokuka * fixes test Signed-off-by: Oleh Dokuka Co-authored-by: Rossen Stoyanchev --- build.gradle | 4 + .../main/java/io/rsocket/RSocketFactory.java | 107 ++- .../main/java/io/rsocket/ReconnectMono.java | 477 ++++++++++ .../java/io/rsocket/RSocketReconnectTest.java | 165 ++++ .../java/io/rsocket/ReconnectMonoTests.java | 868 ++++++++++++++++++ 5 files changed, 1617 insertions(+), 4 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/ReconnectMono.java create mode 100644 rsocket-core/src/test/java/io/rsocket/RSocketReconnectTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/ReconnectMonoTests.java diff --git a/build.gradle b/build.gradle index 262015af6..7e5eb9823 100644 --- a/build.gradle +++ b/build.gradle @@ -119,6 +119,10 @@ subprojects { test { useJUnitPlatform() + testLogging { + events "started", "passed", "skipped", "failed" + } + systemProperty "io.netty.leakDetection.level", "ADVANCED" } diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 44f64e550..6570ed7a3 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -44,10 +44,13 @@ import io.rsocket.util.MultiSubscriberRSocket; import java.time.Duration; import java.util.Objects; +import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; +import reactor.core.Disposable; import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; /** Factory for creating RSocket clients and servers. */ public class RSocketFactory { @@ -95,6 +98,9 @@ default Start transport(ServerTransport transport) { public static class ClientRSocketFactory implements ClientTransportAcceptor { private static final String CLIENT_TAG = "client"; + private static final BiConsumer INVALIDATE_FUNCTION = + (r, i) -> r.onClose().subscribe(null, null, i::invalidate); + private SocketAcceptor acceptor = (setup, sendingSocket) -> Mono.just(new AbstractRSocket() {}); private Consumer errorConsumer = Throwable::printStackTrace; @@ -125,6 +131,8 @@ public static class ClientRSocketFactory implements ClientTransportAcceptor { private boolean multiSubscriberRequester = true; private boolean leaseEnabled; private Supplier> leasesSupplier = Leases::new; + private boolean reconnectEnabled; + private Retry retrySpec; private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; @@ -230,6 +238,86 @@ public ClientRSocketFactory singleSubscriberRequester() { return this; } + /** + * Enables a reconnectable, shared instance of {@code Mono} so every subscriber will + * observe the same RSocket instance up on connection establishment.
+ * For example: + * + *

{@code
+     * Mono sharedRSocketMono =
+     *   RSocketFactory
+     *                .connect()
+     *                .singleSubscriberRequester()
+     *                .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
+     *                .transport(transport)
+     *                .start();
+     *
+     *  RSocket r1 = sharedRSocketMono.block();
+     *  RSocket r2 = sharedRSocketMono.block();
+     *
+     *  assert r1 == r2;
+     *
+     * }
+ * + * Apart of the shared behavior, if the connection is lost, the same {@code Mono} + * instance will transparently re-establish the connection for subsequent subscribers.
+ * For example: + * + *
{@code
+     * Mono sharedRSocketMono =
+     *   RSocketFactory
+     *                .connect()
+     *                .singleSubscriberRequester()
+     *                .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
+     *                .transport(transport)
+     *                .start();
+     *
+     *  RSocket r1 = sharedRSocketMono.block();
+     *  RSocket r2 = sharedRSocketMono.block();
+     *
+     *  assert r1 == r2;
+     *
+     *  r1.dispose()
+     *
+     *  assert r2.isDisposed()
+     *
+     *  RSocket r3 = sharedRSocketMono.block();
+     *  RSocket r4 = sharedRSocketMono.block();
+     *
+     *
+     *  assert r1 != r3;
+     *  assert r4 == r3;
+     *
+     * }
+ * + * Note, having reconnect() enabled does not eliminate the need to accompany each + * individual request with the corresponding retry logic.
+ * For example: + * + *
{@code
+     * Mono sharedRSocketMono =
+     *   RSocketFactory
+     *                .connect()
+     *                .singleSubscriberRequester()
+     *                .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
+     *                .transport(transport)
+     *                .start();
+     *
+     *  sharedRSocket.flatMap(rSocket -> rSocket.requestResponse(...))
+     *               .retryWhen(ownRetry)
+     *               .subscribe()
+     *
+     * }
+ * + * @param retrySpec a retry factory applied for {@link Mono#retryWhen(Retry)} + * @return a shared instance of {@code Mono}. + */ + public ClientRSocketFactory reconnect(Retry retrySpec) { + this.retrySpec = Objects.requireNonNull(retrySpec); + this.reconnectEnabled = true; + return this; + } + public ClientRSocketFactory resume() { this.resumeEnabled = true; return this; @@ -392,6 +480,15 @@ public Mono start() { .sendOne(setupFrame) .thenReturn(wrappedRSocketRequester); }); + }) + .as( + source -> { + if (reconnectEnabled) { + return new ReconnectMono<>( + source.retryWhen(retrySpec), Disposable::dispose, INVALIDATE_FUNCTION); + } else { + return source; + } }); } @@ -422,7 +519,7 @@ private ClientSetup clientSetup(DuplexConnection startConnection) { } private Mono newConnection() { - return transportClient.get().connect(mtu); + return Mono.fromSupplier(transportClient).flatMap(t -> t.connect(mtu)); } } } @@ -698,9 +795,11 @@ public Mono start() { @Override public Mono get() { - return transportServer - .get() - .start(duplexConnection -> acceptor(serverSetup, duplexConnection), mtu) + return Mono.fromSupplier(transportServer) + .flatMap( + transport -> + transport.start( + duplexConnection -> acceptor(serverSetup, duplexConnection), mtu)) .doOnNext(c -> c.onClose().doFinally(v -> serverSetup.dispose()).subscribe()); } }); diff --git a/rsocket-core/src/main/java/io/rsocket/ReconnectMono.java b/rsocket-core/src/main/java/io/rsocket/ReconnectMono.java new file mode 100644 index 000000000..b2c36908d --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/ReconnectMono.java @@ -0,0 +1,477 @@ +/* + * 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; + +import java.time.Duration; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Disposable; +import reactor.core.Exceptions; +import reactor.core.Scannable; +import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.core.publisher.Operators.MonoSubscriber; +import reactor.util.annotation.Nullable; +import reactor.util.context.Context; + +final class ReconnectMono extends Mono implements Invalidatable, Disposable, Scannable { + + final Mono source; + final BiConsumer onValueReceived; + final Consumer onValueExpired; + final ReconnectMainSubscriber mainSubscriber; + + volatile int wip; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(ReconnectMono.class, "wip"); + + volatile ReconnectInner[] subscribers; + + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater SUBSCRIBERS = + AtomicReferenceFieldUpdater.newUpdater( + ReconnectMono.class, ReconnectInner[].class, "subscribers"); + + @SuppressWarnings("rawtypes") + static final ReconnectInner[] EMPTY_UNSUBSCRIBED = new ReconnectInner[0]; + + @SuppressWarnings("rawtypes") + static final ReconnectInner[] EMPTY_SUBSCRIBED = new ReconnectInner[0]; + + @SuppressWarnings("rawtypes") + static final ReconnectInner[] READY = new ReconnectInner[0]; + + @SuppressWarnings("rawtypes") + static final ReconnectInner[] TERMINATED = new ReconnectInner[0]; + + static final int ADDED_STATE = 0; + static final int READY_STATE = 1; + static final int TERMINATED_STATE = 2; + + T value; + Throwable t; + + ReconnectMono( + Mono source, + Consumer onValueExpired, + BiConsumer onValueReceived) { + this.source = source; + this.onValueExpired = onValueExpired; + this.onValueReceived = onValueReceived; + this.mainSubscriber = new ReconnectMainSubscriber<>(this); + + SUBSCRIBERS.lazySet(this, EMPTY_UNSUBSCRIBED); + } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) return source; + if (key == Attr.PREFETCH) return Integer.MAX_VALUE; + + final boolean isDisposed = isDisposed(); + if (key == Attr.TERMINATED) return isDisposed; + if (key == Attr.ERROR) return t; + + return null; + } + + @Override + public void dispose() { + this.terminate(new CancellationException("ReconnectMono has already been disposed")); + } + + @Override + public boolean isDisposed() { + return this.subscribers == TERMINATED; + } + + @Override + @SuppressWarnings("uncheked") + public void subscribe(CoreSubscriber actual) { + final ReconnectInner inner = new ReconnectInner<>(actual, this); + actual.onSubscribe(inner); + + final int state = this.add(inner); + + if (state == READY_STATE) { + inner.complete(this.value); + } else if (state == TERMINATED_STATE) { + inner.onError(this.t); + } + } + + /** + * Block the calling thread indefinitely, waiting for the completion of this {@code + * ReconnectMono}. If the {@link ReconnectMono} is completed with an error a RuntimeException that + * wraps the error is thrown. + * + * @return the value of this {@code ReconnectMono} + */ + @Override + @Nullable + public T block() { + return block(null); + } + + /** + * Block the calling thread for the specified time, waiting for the completion of this {@code + * ReconnectMono}. If the {@link ReconnectMono} is completed with an error a RuntimeException that + * wraps the error is thrown. + * + * @param timeout the timeout value as a {@link Duration} + * @return the value of this {@code ReconnectMono} or {@code null} if the timeout is reached and + * the {@code ReconnectMono} has not completed + */ + @Override + @Nullable + @SuppressWarnings("uncheked") + public T block(@Nullable Duration timeout) { + try { + ReconnectInner[] subscribers = this.subscribers; + if (subscribers == READY) { + return this.value; + } + + if (subscribers == TERMINATED) { + RuntimeException re = Exceptions.propagate(this.t); + re = Exceptions.addSuppressed(re, new Exception("ReconnectMono terminated with an error")); + throw re; + } + + // connect once + if (subscribers == EMPTY_UNSUBSCRIBED + && SUBSCRIBERS.compareAndSet(this, EMPTY_UNSUBSCRIBED, EMPTY_SUBSCRIBED)) { + this.source.subscribe(this.mainSubscriber); + } + + long delay; + if (null == timeout) { + delay = 0L; + } else { + delay = System.nanoTime() + timeout.toNanos(); + } + for (; ; ) { + ReconnectInner[] inners = this.subscribers; + + if (inners == READY) { + return this.value; + } + if (inners == TERMINATED) { + RuntimeException re = Exceptions.propagate(this.t); + re = + Exceptions.addSuppressed(re, new Exception("ReconnectMono terminated with an error")); + throw re; + } + if (timeout != null && delay < System.nanoTime()) { + throw new IllegalStateException("Timeout on Mono blocking read"); + } + + Thread.sleep(1); + } + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + + throw new IllegalStateException("Thread Interruption on Mono blocking read"); + } + } + + @SuppressWarnings("unchecked") + void terminate(Throwable t) { + if (isDisposed()) { + return; + } + + // writes happens before volatile write + this.t = t; + + final ReconnectInner[] subscribers = SUBSCRIBERS.getAndSet(this, TERMINATED); + if (subscribers == TERMINATED) { + Operators.onErrorDropped(t, Context.empty()); + return; + } + + this.mainSubscriber.dispose(); + + this.doFinally(); + + for (CoreSubscriber consumer : subscribers) { + consumer.onError(t); + } + } + + void complete() { + ReconnectInner[] subscribers = this.subscribers; + if (subscribers == TERMINATED) { + return; + } + + final T value = this.value; + + for (; ; ) { + // ensures TERMINATE is going to be replaced with READY + if (SUBSCRIBERS.compareAndSet(this, subscribers, READY)) { + break; + } + + subscribers = this.subscribers; + + if (subscribers == TERMINATED) { + this.doFinally(); + return; + } + } + + this.onValueReceived.accept(value, this); + + for (ReconnectInner consumer : subscribers) { + consumer.complete(value); + } + } + + void doFinally() { + if (WIP.getAndIncrement(this) != 0) { + return; + } + + int m = 1; + T value; + + for (; ; ) { + value = this.value; + + if (value != null && isDisposed()) { + this.value = null; + this.onValueExpired.accept(value); + return; + } + + m = WIP.addAndGet(this, -m); + if (m == 0) { + return; + } + } + } + + // Check RSocket is not good + @Override + public void invalidate() { + if (this.subscribers == TERMINATED) { + return; + } + + final ReconnectInner[] subscribers = this.subscribers; + + if (subscribers == READY && SUBSCRIBERS.compareAndSet(this, READY, EMPTY_UNSUBSCRIBED)) { + final T value = this.value; + this.value = null; + + if (value != null) { + this.onValueExpired.accept(value); + } + } + } + + int add(ReconnectInner ps) { + for (; ; ) { + ReconnectInner[] a = this.subscribers; + + if (a == TERMINATED) { + return TERMINATED_STATE; + } + + if (a == READY) { + return READY_STATE; + } + + int n = a.length; + @SuppressWarnings("unchecked") + ReconnectInner[] b = new ReconnectInner[n + 1]; + System.arraycopy(a, 0, b, 0, n); + b[n] = ps; + + if (SUBSCRIBERS.compareAndSet(this, a, b)) { + if (a == EMPTY_UNSUBSCRIBED) { + this.source.subscribe(this.mainSubscriber); + } + return ADDED_STATE; + } + } + } + + @SuppressWarnings("unchecked") + void remove(ReconnectInner ps) { + for (; ; ) { + ReconnectInner[] a = this.subscribers; + int n = a.length; + if (n == 0) { + return; + } + + int j = -1; + for (int i = 0; i < n; i++) { + if (a[i] == ps) { + j = i; + break; + } + } + + if (j < 0) { + return; + } + + ReconnectInner[] b; + + if (n == 1) { + b = EMPTY_SUBSCRIBED; + } else { + b = new ReconnectInner[n - 1]; + System.arraycopy(a, 0, b, 0, j); + System.arraycopy(a, j + 1, b, j, n - j - 1); + } + if (SUBSCRIBERS.compareAndSet(this, a, b)) { + return; + } + } + } + + static final class ReconnectMainSubscriber implements CoreSubscriber { + + final ReconnectMono parent; + + volatile Subscription s; + + @SuppressWarnings("rawtypes") + static final AtomicReferenceFieldUpdater S = + AtomicReferenceFieldUpdater.newUpdater( + ReconnectMainSubscriber.class, Subscription.class, "s"); + + ReconnectMainSubscriber(ReconnectMono parent) { + this.parent = parent; + } + + @Override + public void onSubscribe(Subscription s) { + if (Operators.setOnce(S, this, s)) { + s.request(Long.MAX_VALUE); + } + } + + @Override + public void onComplete() { + final Subscription s = this.s; + final ReconnectMono p = this.parent; + final T value = p.value; + + if (s == Operators.cancelledSubscription() || !S.compareAndSet(this, s, null)) { + p.doFinally(); + return; + } + + if (value == null) { + p.terminate(new IllegalStateException("Unexpected Completion of the Upstream")); + } else { + p.complete(); + } + } + + @Override + public void onError(Throwable t) { + final Subscription s = this.s; + final ReconnectMono p = this.parent; + + if (s == Operators.cancelledSubscription() + || S.getAndSet(this, Operators.cancelledSubscription()) + == Operators.cancelledSubscription()) { + p.doFinally(); + Operators.onErrorDropped(t, Context.empty()); + return; + } + + // terminate upstream which means retryBackoff has exhausted + p.terminate(t); + } + + @Override + public void onNext(T value) { + if (this.s == Operators.cancelledSubscription()) { + this.parent.onValueExpired.accept(value); + return; + } + + final ReconnectMono p = this.parent; + + p.value = value; + // volatile write and check on racing + p.doFinally(); + } + + void dispose() { + Operators.terminate(S, this); + } + } + + static final class ReconnectInner extends MonoSubscriber { + final ReconnectMono parent; + + ReconnectInner(CoreSubscriber actual, ReconnectMono parent) { + super(actual); + this.parent = parent; + } + + @Override + public void cancel() { + if (!isCancelled()) { + super.cancel(); + this.parent.remove(this); + } + } + + @Override + public void onComplete() { + if (!isCancelled()) { + this.actual.onComplete(); + } + } + + @Override + public void onError(Throwable t) { + if (isCancelled()) { + Operators.onErrorDropped(t, currentContext()); + } else { + this.actual.onError(t); + } + } + + @Override + public Object scanUnsafe(Attr key) { + if (key == Attr.PARENT) return this.parent; + return super.scanUnsafe(key); + } + } +} + +interface Invalidatable { + + void invalidate(); +} diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketReconnectTest.java b/rsocket-core/src/test/java/io/rsocket/RSocketReconnectTest.java new file mode 100644 index 000000000..b5ca4858e --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/RSocketReconnectTest.java @@ -0,0 +1,165 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; + +import io.rsocket.test.util.TestClientTransport; +import io.rsocket.transport.ClientTransport; +import java.io.UncheckedIOException; +import java.time.Duration; +import java.util.Iterator; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.function.Consumer; +import java.util.function.Supplier; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.core.Exceptions; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +public class RSocketReconnectTest { + + private Queue retries = new ConcurrentLinkedQueue<>(); + + @Test + public void shouldBeASharedReconnectableInstanceOfRSocketMono() { + TestClientTransport[] testClientTransport = + new TestClientTransport[] {new TestClientTransport()}; + Mono rSocketMono = + RSocketFactory.connect() + .singleSubscriberRequester() + .reconnect(Retry.indefinitely()) + .transport( + () -> { + return testClientTransport[0]; + }) + .start(); + + RSocket rSocket1 = rSocketMono.block(); + RSocket rSocket2 = rSocketMono.block(); + + Assertions.assertThat(rSocket1).isEqualTo(rSocket2); + + testClientTransport[0].testConnection().dispose(); + testClientTransport[0] = new TestClientTransport(); + + RSocket rSocket3 = rSocketMono.block(); + RSocket rSocket4 = rSocketMono.block(); + + Assertions.assertThat(rSocket3).isEqualTo(rSocket4).isNotEqualTo(rSocket2); + } + + @Test + @SuppressWarnings({"rawtype", "unchecked"}) + public void shouldBeRetrieableConnectionSharedReconnectableInstanceOfRSocketMono() { + Supplier mockTransportSupplier = Mockito.mock(Supplier.class); + Mockito.when(mockTransportSupplier.get()) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenReturn(new TestClientTransport()); + Mono rSocketMono = + RSocketFactory.connect() + .singleSubscriberRequester() + .reconnect( + Retry.backoff(4, Duration.ofMillis(100)) + .maxBackoff(Duration.ofMillis(500)) + .doAfterRetry(onRetry())) + .transport(mockTransportSupplier) + .start(); + + RSocket rSocket1 = rSocketMono.block(); + RSocket rSocket2 = rSocketMono.block(); + + Assertions.assertThat(rSocket1).isEqualTo(rSocket2); + assertRetries( + UncheckedIOException.class, + UncheckedIOException.class, + UncheckedIOException.class, + UncheckedIOException.class); + } + + @Test + @SuppressWarnings({"rawtype", "unchecked"}) + public void shouldBeExaustedRetrieableConnectionSharedReconnectableInstanceOfRSocketMono() { + Supplier mockTransportSupplier = Mockito.mock(Supplier.class); + Mockito.when(mockTransportSupplier.get()) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenThrow(UncheckedIOException.class) + .thenReturn(new TestClientTransport()); + Mono rSocketMono = + RSocketFactory.connect() + .singleSubscriberRequester() + .reconnect( + Retry.backoff(4, Duration.ofMillis(100)) + .maxBackoff(Duration.ofMillis(500)) + .doAfterRetry(onRetry())) + .transport(mockTransportSupplier) + .start(); + + Assertions.assertThatThrownBy(rSocketMono::block) + .matches(Exceptions::isRetryExhausted) + .hasCauseInstanceOf(UncheckedIOException.class); + + Assertions.assertThatThrownBy(rSocketMono::block) + .matches(Exceptions::isRetryExhausted) + .hasCauseInstanceOf(UncheckedIOException.class); + + assertRetries( + UncheckedIOException.class, + UncheckedIOException.class, + UncheckedIOException.class, + UncheckedIOException.class); + } + + @Test + public void shouldBeNotBeASharedReconnectableInstanceOfRSocketMono() { + + Mono rSocketMono = + RSocketFactory.connect() + .singleSubscriberRequester() + .transport(new TestClientTransport()) + .start(); + + RSocket rSocket1 = rSocketMono.block(); + RSocket rSocket2 = rSocketMono.block(); + + Assertions.assertThat(rSocket1).isNotEqualTo(rSocket2); + } + + @SafeVarargs + private final void assertRetries(Class... exceptions) { + assertEquals(exceptions.length, retries.size()); + int index = 0; + for (Iterator it = retries.iterator(); it.hasNext(); ) { + Retry.RetrySignal retryContext = it.next(); + assertEquals(index, retryContext.totalRetries()); + assertEquals(exceptions[index], retryContext.failure().getClass()); + index++; + } + } + + Consumer onRetry() { + return context -> retries.add(context); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/ReconnectMonoTests.java b/rsocket-core/src/test/java/io/rsocket/ReconnectMonoTests.java new file mode 100644 index 000000000..fb7707509 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/ReconnectMonoTests.java @@ -0,0 +1,868 @@ +/* + * 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; + +import static org.junit.Assert.assertEquals; + +import java.io.IOException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeoutException; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import org.assertj.core.api.Assertions; +import org.junit.Test; +import org.mockito.Mockito; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Exceptions; +import reactor.core.Scannable; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; +import reactor.core.scheduler.Schedulers; +import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; +import reactor.test.util.RaceTestUtils; +import reactor.util.function.Tuple2; +import reactor.util.function.Tuples; +import reactor.util.retry.Retry; + +public class ReconnectMonoTests { + + private Queue retries = new ConcurrentLinkedQueue<>(); + private Queue> received = new ConcurrentLinkedQueue<>(); + private Queue expired = new ConcurrentLinkedQueue<>(); + + @Test + public void shouldExpireValueOnRacingDisposeAndNext() { + Hooks.onErrorDropped(t -> {}); + Hooks.onNextDropped(System.out::println); + for (int i = 0; i < 100000; i++) { + final int index = i; + final CoreSubscriber[] monoSubscribers = new CoreSubscriber[1]; + Subscription mockSubscription = Mockito.mock(Subscription.class); + final Mono stringMono = + new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + actual.onSubscribe(mockSubscription); + monoSubscribers[0] = actual; + } + }; + + final ReconnectMono reconnectMono = + stringMono + .doOnDiscard(Object.class, System.out::println) + .as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + RaceTestUtils.race(() -> monoSubscribers[0].onNext("value" + index), reconnectMono::dispose); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + Mockito.verify(mockSubscription).cancel(); + + if (processor.isError()) { + Assertions.assertThat(processor.getError()) + .isInstanceOf(CancellationException.class) + .hasMessage("ReconnectMono has already been disposed"); + + Assertions.assertThat(expired).containsOnly("value" + i); + } else { + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + } + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldNotifyAllTheSubscribersUnderRacingBetweenSubscribeAndComplete() { + Hooks.onErrorDropped(t -> {}); + for (int i = 0; i < 100000; i++) { + final TestPublisher cold = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + final MonoProcessor racerProcessor = MonoProcessor.create(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + cold.next("value" + i); + + RaceTestUtils.race(cold::complete, () -> reconnectMono.subscribe(racerProcessor)); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + Assertions.assertThat(racerProcessor.peek()).isEqualTo("value" + i); + + Assertions.assertThat(reconnectMono.subscribers).isEqualTo(ReconnectMono.READY); + + Assertions.assertThat( + reconnectMono.add(new ReconnectMono.ReconnectInner<>(processor, reconnectMono))) + .isEqualTo(ReconnectMono.READY_STATE); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + + received.clear(); + } + } + + @Test + public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribers() { + for (int i = 0; i < 100000; i++) { + final TestPublisher cold = TestPublisher.createCold(); + cold.next("value" + i); + + final ReconnectMono reconnectMono = + cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor processor = MonoProcessor.create(); + final MonoProcessor racerProcessor = MonoProcessor.create(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + Assertions.assertThat(cold.subscribeCount()).isZero(); + + RaceTestUtils.race( + () -> reconnectMono.subscribe(processor), () -> reconnectMono.subscribe(racerProcessor)); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + Assertions.assertThat(racerProcessor.isTerminated()).isTrue(); + + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + Assertions.assertThat(racerProcessor.peek()).isEqualTo("value" + i); + + Assertions.assertThat(reconnectMono.subscribers).isEqualTo(ReconnectMono.READY); + + Assertions.assertThat(cold.subscribeCount()).isOne(); + + Assertions.assertThat( + reconnectMono.add(new ReconnectMono.ReconnectInner<>(processor, reconnectMono))) + .isEqualTo(ReconnectMono.READY_STATE); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + + received.clear(); + } + } + + @Test + public void shouldEstablishValueOnceInCaseOfRacingBetweenSubscribeAndBlock() { + Duration timeout = Duration.ofMillis(100); + for (int i = 0; i < 100000; i++) { + final TestPublisher cold = TestPublisher.createCold(); + cold.next("value" + i); + + final ReconnectMono reconnectMono = + cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor processor = MonoProcessor.create(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + Assertions.assertThat(cold.subscribeCount()).isZero(); + + String[] values = new String[1]; + + RaceTestUtils.race( + () -> values[0] = reconnectMono.block(timeout), () -> reconnectMono.subscribe(processor)); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + Assertions.assertThat(values).containsExactly("value" + i); + + Assertions.assertThat(reconnectMono.subscribers).isEqualTo(ReconnectMono.READY); + + Assertions.assertThat(cold.subscribeCount()).isOne(); + + Assertions.assertThat( + reconnectMono.add(new ReconnectMono.ReconnectInner<>(processor, reconnectMono))) + .isEqualTo(ReconnectMono.READY_STATE); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + + received.clear(); + } + } + + @Test + public void shouldEstablishValueOnceInCaseOfRacingBetweenBlocks() { + Duration timeout = Duration.ofMillis(100); + for (int i = 0; i < 100000; i++) { + final TestPublisher cold = TestPublisher.createCold(); + cold.next("value" + i); + + final ReconnectMono reconnectMono = + cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + Assertions.assertThat(cold.subscribeCount()).isZero(); + + String[] values1 = new String[1]; + String[] values2 = new String[1]; + + RaceTestUtils.race( + () -> values1[0] = reconnectMono.block(timeout), + () -> values2[0] = reconnectMono.block(timeout)); + + Assertions.assertThat(values2).containsExactly("value" + i); + Assertions.assertThat(values1).containsExactly("value" + i); + + Assertions.assertThat(reconnectMono.subscribers).isEqualTo(ReconnectMono.READY); + + Assertions.assertThat(cold.subscribeCount()).isOne(); + + Assertions.assertThat( + reconnectMono.add( + new ReconnectMono.ReconnectInner<>(MonoProcessor.create(), reconnectMono))) + .isEqualTo(ReconnectMono.READY_STATE); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + + received.clear(); + } + } + + @Test + public void shouldExpireValueOnRacingDisposeAndNoValueComplete() { + Hooks.onErrorDropped(t -> {}); + for (int i = 0; i < 100000; i++) { + final TestPublisher cold = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + RaceTestUtils.race(cold::complete, reconnectMono::dispose); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + + Throwable error = processor.getError(); + + if (error instanceof CancellationException) { + Assertions.assertThat(error) + .isInstanceOf(CancellationException.class) + .hasMessage("ReconnectMono has already been disposed"); + } else { + Assertions.assertThat(error) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Unexpected Completion of the Upstream"); + } + + Assertions.assertThat(expired).isEmpty(); + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldExpireValueOnRacingDisposeAndComplete() { + Hooks.onErrorDropped(t -> {}); + for (int i = 0; i < 100000; i++) { + final TestPublisher cold = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + cold.next("value" + i); + + RaceTestUtils.race(cold::complete, reconnectMono::dispose); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + + if (processor.isError()) { + Assertions.assertThat(processor.getError()) + .isInstanceOf(CancellationException.class) + .hasMessage("ReconnectMono has already been disposed"); + } else { + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + } + + Assertions.assertThat(expired).hasSize(1).containsOnly("value" + i); + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldExpireValueOnRacingDisposeAndError() { + Hooks.onErrorDropped(t -> {}); + RuntimeException runtimeException = new RuntimeException("test"); + for (int i = 0; i < 100000; i++) { + final TestPublisher cold = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + cold.next("value" + i); + + RaceTestUtils.race(() -> cold.error(runtimeException), reconnectMono::dispose); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + + if (processor.isError()) { + if (processor.getError() instanceof CancellationException) { + Assertions.assertThat(processor.getError()) + .isInstanceOf(CancellationException.class) + .hasMessage("ReconnectMono has already been disposed"); + } else { + Assertions.assertThat(processor.getError()) + .isInstanceOf(RuntimeException.class) + .hasMessage("test"); + } + } else { + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + } + + Assertions.assertThat(expired).hasSize(1).containsOnly("value" + i); + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldExpireValueOnRacingDisposeAndErrorWithNoBackoff() { + Hooks.onErrorDropped(t -> {}); + RuntimeException runtimeException = new RuntimeException("test"); + for (int i = 0; i < 100000; i++) { + final TestPublisher cold = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + cold.mono() + .retryWhen(Retry.max(1).filter(t -> t instanceof Exception)) + .as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + + cold.next("value" + i); + + RaceTestUtils.race(() -> cold.error(runtimeException), reconnectMono::dispose); + + Assertions.assertThat(processor.isTerminated()).isTrue(); + + if (processor.isError()) { + + if (processor.getError() instanceof CancellationException) { + Assertions.assertThat(processor.getError()) + .isInstanceOf(CancellationException.class) + .hasMessage("ReconnectMono has already been disposed"); + } else { + Assertions.assertThat(processor.getError()) + .matches(t -> Exceptions.isRetryExhausted(t)) + .hasCause(runtimeException); + } + + Assertions.assertThat(expired).hasSize(1).containsOnly("value" + i); + } else { + Assertions.assertThat(received) + .hasSize(1) + .containsOnly(Tuples.of("value" + i, reconnectMono)); + Assertions.assertThat(processor.peek()).isEqualTo("value" + i); + } + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldThrowOnBlocking() { + final TestPublisher publisher = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + Assertions.assertThatThrownBy(() -> reconnectMono.block(Duration.ofMillis(100))) + .isInstanceOf(IllegalStateException.class) + .hasMessage("Timeout on Mono blocking read"); + } + + @Test + public void shouldThrowOnBlockingIfHasAlreadyTerminated() { + final TestPublisher publisher = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + publisher.error(new RuntimeException("test")); + + Assertions.assertThatThrownBy(() -> reconnectMono.block(Duration.ofMillis(100))) + .isInstanceOf(RuntimeException.class) + .hasMessage("test") + .hasSuppressedException(new Exception("ReconnectMono terminated with an error")); + } + + @Test + public void shouldBeScannable() { + final TestPublisher publisher = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final Mono parent = publisher.mono(); + final ReconnectMono reconnectMono = + parent.as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final Scannable scannableOfReconnect = Scannable.from(reconnectMono); + + Assertions.assertThat( + (List) + scannableOfReconnect.parents().map(s -> s.getClass()).collect(Collectors.toList())) + .hasSize(1) + .containsExactly(publisher.mono().getClass()); + Assertions.assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.TERMINATED)) + .isEqualTo(false); + Assertions.assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.ERROR)).isNull(); + + final MonoProcessor processor = reconnectMono.subscribeWith(MonoProcessor.create()); + + final Scannable scannableOfMonoProcessor = Scannable.from(processor); + + Assertions.assertThat( + (List) + scannableOfMonoProcessor + .parents() + .map(s -> s.getClass()) + .collect(Collectors.toList())) + .hasSize(3) + .containsExactly( + ReconnectMono.ReconnectInner.class, ReconnectMono.class, publisher.mono().getClass()); + + reconnectMono.dispose(); + + Assertions.assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.TERMINATED)) + .isEqualTo(true); + Assertions.assertThat(scannableOfReconnect.scanUnsafe(Scannable.Attr.ERROR)) + .isInstanceOf(CancellationException.class); + } + + @Test + public void shouldNotExpiredIfNotCompleted() { + final TestPublisher publisher = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + MonoProcessor processor = MonoProcessor.create(); + + reconnectMono.subscribe(processor); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + + publisher.next("test"); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + + reconnectMono.invalidate(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + publisher.assertSubscribers(1); + Assertions.assertThat(publisher.subscribeCount()).isEqualTo(1); + + publisher.complete(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).hasSize(1); + Assertions.assertThat(processor.isTerminated()).isTrue(); + + publisher.assertSubscribers(0); + Assertions.assertThat(publisher.subscribeCount()).isEqualTo(1); + } + + @Test + public void shouldNotEmitUntilCompletion() { + final TestPublisher publisher = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + MonoProcessor processor = MonoProcessor.create(); + + reconnectMono.subscribe(processor); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + + publisher.next("test"); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + + publisher.complete(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).hasSize(1); + Assertions.assertThat(processor.isTerminated()).isTrue(); + Assertions.assertThat(processor.peek()).isEqualTo("test"); + } + + @Test + public void shouldBePossibleToRemoveThemSelvesFromTheList_CancellationTest() { + final TestPublisher publisher = + TestPublisher.createNoncompliant(TestPublisher.Violation.REQUEST_OVERFLOW); + // given + final int minBackoff = 1; + final int maxBackoff = 5; + final int timeout = 10; + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + MonoProcessor processor = MonoProcessor.create(); + + reconnectMono.subscribe(processor); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + + publisher.next("test"); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(processor.isTerminated()).isFalse(); + + processor.cancel(); + + Assertions.assertThat(reconnectMono.subscribers).isEqualTo(ReconnectMono.EMPTY_SUBSCRIBED); + + publisher.complete(); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).hasSize(1); + Assertions.assertThat(processor.isTerminated()).isFalse(); + Assertions.assertThat(processor.peek()).isNull(); + } + + @Test + public void shouldExpireValueOnDispose() { + final TestPublisher publisher = TestPublisher.create(); + // given + final int minBackoff = 1; + final int maxBackoff = 5; + final int timeout = 10; + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + StepVerifier.create(reconnectMono) + .expectSubscription() + .then(() -> publisher.next("value")) + .expectNext("value") + .expectComplete() + .verify(Duration.ofSeconds(timeout)); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).hasSize(1); + + reconnectMono.dispose(); + + Assertions.assertThat(expired).hasSize(1); + Assertions.assertThat(received).hasSize(1); + Assertions.assertThat(reconnectMono.isDisposed()).isTrue(); + + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + .expectSubscription() + .expectError(CancellationException.class) + .verify(Duration.ofSeconds(timeout)); + } + + @Test + public void shouldNotifyAllTheSubscribers() { + final TestPublisher publisher = TestPublisher.create(); + // given + final int minBackoff = 1; + final int maxBackoff = 5; + final int timeout = 10; + + final ReconnectMono reconnectMono = + publisher.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + final MonoProcessor sub1 = MonoProcessor.create(); + final MonoProcessor sub2 = MonoProcessor.create(); + final MonoProcessor sub3 = MonoProcessor.create(); + final MonoProcessor sub4 = MonoProcessor.create(); + + reconnectMono.subscribe(sub1); + reconnectMono.subscribe(sub2); + reconnectMono.subscribe(sub3); + reconnectMono.subscribe(sub4); + + Assertions.assertThat(reconnectMono.subscribers).hasSize(4); + + final ArrayList> processors = new ArrayList<>(200); + + for (int i = 0; i < 100; i++) { + final MonoProcessor subA = MonoProcessor.create(); + final MonoProcessor subB = MonoProcessor.create(); + processors.add(subA); + processors.add(subB); + RaceTestUtils.race(() -> reconnectMono.subscribe(subA), () -> reconnectMono.subscribe(subB)); + } + + Assertions.assertThat(reconnectMono.subscribers).hasSize(204); + + sub1.dispose(); + + Assertions.assertThat(reconnectMono.subscribers).hasSize(203); + + publisher.next("value"); + + Assertions.assertThatThrownBy(sub1::peek).isInstanceOf(CancellationException.class); + Assertions.assertThat(sub2.peek()).isEqualTo("value"); + Assertions.assertThat(sub3.peek()).isEqualTo("value"); + Assertions.assertThat(sub4.peek()).isEqualTo("value"); + + for (MonoProcessor sub : processors) { + Assertions.assertThat(sub.peek()).isEqualTo("value"); + Assertions.assertThat(sub.isTerminated()).isTrue(); + } + + Assertions.assertThat(publisher.subscribeCount()).isEqualTo(1); + } + + @Test + public void shouldExpireValueExactlyOnce() { + for (int i = 0; i < 1000; i++) { + final TestPublisher cold = TestPublisher.createCold(); + cold.next("value"); + // given + final int minBackoff = 1; + final int maxBackoff = 5; + final int timeout = 10; + + final ReconnectMono reconnectMono = + cold.mono().as(source -> new ReconnectMono<>(source, onExpire(), onValue())); + + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + .expectSubscription() + .expectNext("value") + .expectComplete() + .verify(Duration.ofSeconds(timeout)); + + Assertions.assertThat(expired).isEmpty(); + Assertions.assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); + RaceTestUtils.race(reconnectMono::invalidate, reconnectMono::invalidate); + + Assertions.assertThat(expired).hasSize(1).containsOnly("value"); + Assertions.assertThat(received).hasSize(1).containsOnly(Tuples.of("value", reconnectMono)); + + StepVerifier.create(reconnectMono.subscribeOn(Schedulers.elastic())) + .expectSubscription() + .expectNext("value") + .expectComplete() + .verify(Duration.ofSeconds(timeout)); + + Assertions.assertThat(expired).hasSize(1).containsOnly("value"); + Assertions.assertThat(received) + .hasSize(2) + .containsOnly(Tuples.of("value", reconnectMono), Tuples.of("value", reconnectMono)); + + Assertions.assertThat(cold.subscribeCount()).isEqualTo(2); + + expired.clear(); + received.clear(); + } + } + + @Test + public void shouldTimeoutRetryWithVirtualTime() { + // given + final int minBackoff = 1; + final int maxBackoff = 5; + final int timeout = 10; + + // then + StepVerifier.withVirtualTime( + () -> + Mono.error(new RuntimeException("Something went wrong")) + .retryWhen( + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(minBackoff)) + .doAfterRetry(onRetry()) + .maxBackoff(Duration.ofSeconds(maxBackoff))) + .timeout(Duration.ofSeconds(timeout)) + .as(m -> new ReconnectMono<>(m, onExpire(), onValue())) + .subscribeOn(Schedulers.elastic())) + .expectSubscription() + .thenAwait(Duration.ofSeconds(timeout)) + .expectError(TimeoutException.class) + .verify(Duration.ofSeconds(timeout)); + + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(expired).isEmpty(); + } + + @Test + public void monoRetryNoBackoff() { + Mono mono = + Mono.error(new IOException()) + .retryWhen(Retry.max(2).doAfterRetry(onRetry())) + .as(m -> new ReconnectMono<>(m, onExpire(), onValue())); + + StepVerifier.create(mono).verifyErrorMatches(Exceptions::isRetryExhausted); + assertRetries(IOException.class, IOException.class); + + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(expired).isEmpty(); + } + + @Test + public void monoRetryFixedBackoff() { + Mono mono = + Mono.error(new IOException()) + .retryWhen(Retry.fixedDelay(1, Duration.ofMillis(500)).doAfterRetry(onRetry())) + .as(m -> new ReconnectMono<>(m, onExpire(), onValue())); + + StepVerifier.withVirtualTime(() -> mono) + .expectSubscription() + .expectNoEvent(Duration.ofMillis(300)) + .thenAwait(Duration.ofMillis(300)) + .verifyErrorMatches(Exceptions::isRetryExhausted); + + assertRetries(IOException.class); + + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(expired).isEmpty(); + } + + @Test + public void monoRetryExponentialBackoff() { + Mono mono = + Mono.error(new IOException()) + .retryWhen( + Retry.backoff(4, Duration.ofMillis(100)) + .maxBackoff(Duration.ofMillis(500)) + .jitter(0.0d) + .doAfterRetry(onRetry())) + .as(m -> new ReconnectMono<>(m, onExpire(), onValue())); + + StepVerifier.withVirtualTime(() -> mono) + .expectSubscription() + .thenAwait(Duration.ofMillis(100)) + .thenAwait(Duration.ofMillis(200)) + .thenAwait(Duration.ofMillis(400)) + .thenAwait(Duration.ofMillis(500)) + .verifyErrorMatches(Exceptions::isRetryExhausted); + + assertRetries(IOException.class, IOException.class, IOException.class, IOException.class); + + Assertions.assertThat(received).isEmpty(); + Assertions.assertThat(expired).isEmpty(); + } + + Consumer onRetry() { + return context -> retries.add(context); + } + + BiConsumer onValue() { + return (v, __) -> received.add(Tuples.of(v, __)); + } + + Consumer onExpire() { + return (v) -> expired.add(v); + } + + @SafeVarargs + private final void assertRetries(Class... exceptions) { + assertEquals(exceptions.length, retries.size()); + int index = 0; + for (Iterator it = retries.iterator(); it.hasNext(); ) { + Retry.RetrySignal retryContext = it.next(); + assertEquals(index, retryContext.totalRetries()); + assertEquals(exceptions[index], retryContext.failure().getClass()); + index++; + } + } + + static boolean isRetryExhausted(Throwable e, Class cause) { + return Exceptions.isRetryExhausted(e) && cause.isInstance(e.getCause()); + } +} From a8df31622d2136f612b5fdc4866652118f77650f Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 27 Mar 2020 12:22:38 +0200 Subject: [PATCH 128/181] provides support for the JDK 14 based builds (#763) Signed-off-by: Oleh Dokuka --- .travis.yml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a40bdf55e..4722957c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,7 +23,7 @@ matrix: - jdk: openjdk8 - jdk: openjdk11 env: SKIP_RELEASE=true - - jdk: openjdk13 + - jdk: openjdk14 env: SKIP_RELEASE=true env: diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 94920145f..a4b442974 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From c76fdb037dbbee2409fb4f115c40c73953f21ab5 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 5 Apr 2020 18:02:24 +0300 Subject: [PATCH 129/181] eliminates deprecations for websocket transport Signed-off-by: Oleh Dokuka --- .../client/WebsocketClientTransport.java | 1 - .../netty/server/WebsocketRouteTransport.java | 134 +----------------- .../server/WebsocketServerTransport.java | 8 +- 3 files changed, 10 insertions(+), 133 deletions(-) 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 5049119a5..62bbd9b99 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 @@ -43,7 +43,6 @@ */ public final class WebsocketClientTransport implements ClientTransport, TransportHeaderAware { - private static final int DEFAULT_FRAME_SIZE = 65536; private static final String DEFAULT_PATH = "/"; private final HttpClient client; diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java index 30aa0fa96..60a34c9b1 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java @@ -19,26 +19,20 @@ import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; import io.netty.buffer.ByteBufAllocator; -import io.netty.handler.codec.http.HttpMethod; import io.rsocket.Closeable; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.WebsocketDuplexConnection; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.function.BiFunction; import java.util.function.Consumer; -import java.util.regex.Matcher; -import java.util.regex.Pattern; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import reactor.netty.Connection; import reactor.netty.http.server.HttpServer; import reactor.netty.http.server.HttpServerRoutes; +import reactor.netty.http.server.WebsocketServerSpec; import reactor.netty.http.websocket.WebsocketInbound; import reactor.netty.http.websocket.WebsocketOutbound; @@ -48,7 +42,7 @@ */ public final class WebsocketRouteTransport extends BaseWebsocketServerTransport { - private final UriPathTemplate template; + private final String path; private final Consumer routesBuilder; @@ -65,7 +59,7 @@ public WebsocketRouteTransport( HttpServer server, Consumer routesBuilder, String path) { this.server = serverConfigurer.apply(Objects.requireNonNull(server, "server must not be null")); this.routesBuilder = Objects.requireNonNull(routesBuilder, "routesBuilder must not be null"); - this.template = new UriPathTemplate(Objects.requireNonNull(path, "path must not be null")); + this.path = Objects.requireNonNull(path, "path must not be null"); } @Override @@ -77,10 +71,9 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { routes -> { routesBuilder.accept(routes); routes.ws( - hsr -> hsr.method().equals(HttpMethod.GET) && template.matches(hsr.uri()), + path, newHandler(acceptor, mtu), - null, - FRAME_LENGTH_MASK); + WebsocketServerSpec.builder().maxFramePayloadLength(FRAME_LENGTH_MASK).build()); }) .bind() .map(CloseableChannel::new); @@ -118,121 +111,4 @@ public static BiFunction> n return acceptor.apply(connection).then(out.neverComplete()); }; } - - static final class UriPathTemplate { - - private static final Pattern FULL_SPLAT_PATTERN = Pattern.compile("[\\*][\\*]"); - private static final String FULL_SPLAT_REPLACEMENT = ".*"; - - private static final Pattern NAME_SPLAT_PATTERN = Pattern.compile("\\{([^/]+?)\\}[\\*][\\*]"); - private static final String NAME_SPLAT_REPLACEMENT = "(?<%NAME%>.*)"; - - private static final Pattern NAME_PATTERN = Pattern.compile("\\{([^/]+?)\\}"); - private static final String NAME_REPLACEMENT = "(?<%NAME%>[^\\/]*)"; - - private final List pathVariables = new ArrayList<>(); - private final HashMap matchers = new HashMap<>(); - private final HashMap> vars = new HashMap<>(); - - private final Pattern uriPattern; - - static String filterQueryParams(String uri) { - int hasQuery = uri.lastIndexOf("?"); - if (hasQuery != -1) { - return uri.substring(0, hasQuery); - } else { - return uri; - } - } - - /** - * Creates a new {@code UriPathTemplate} from the given {@code uriPattern}. - * - * @param uriPattern The pattern to be used by the template - */ - UriPathTemplate(String uriPattern) { - String s = "^" + filterQueryParams(uriPattern); - - Matcher m = NAME_SPLAT_PATTERN.matcher(s); - while (m.find()) { - for (int i = 1; i <= m.groupCount(); i++) { - String name = m.group(i); - pathVariables.add(name); - s = m.replaceFirst(NAME_SPLAT_REPLACEMENT.replaceAll("%NAME%", name)); - m.reset(s); - } - } - - m = NAME_PATTERN.matcher(s); - while (m.find()) { - for (int i = 1; i <= m.groupCount(); i++) { - String name = m.group(i); - pathVariables.add(name); - s = m.replaceFirst(NAME_REPLACEMENT.replaceAll("%NAME%", name)); - m.reset(s); - } - } - - m = FULL_SPLAT_PATTERN.matcher(s); - while (m.find()) { - s = m.replaceAll(FULL_SPLAT_REPLACEMENT); - m.reset(s); - } - - this.uriPattern = Pattern.compile(s + "$"); - } - - /** - * Tests the given {@code uri} against this template, returning {@code true} if the uri matches - * the template, {@code false} otherwise. - * - * @param uri The uri to match - * @return {@code true} if there's a match, {@code false} otherwise - */ - public boolean matches(String uri) { - return matcher(uri).matches(); - } - - /** - * Matches the template against the given {@code uri} returning a map of path parameters - * extracted from the uri, keyed by the names in the template. If the uri does not match, or - * there are no path parameters, an empty map is returned. - * - * @param uri The uri to match - * @return the path parameters from the uri. Never {@code null}. - */ - final Map match(String uri) { - Map pathParameters = vars.get(uri); - if (null != pathParameters) { - return pathParameters; - } - - pathParameters = new HashMap<>(); - Matcher m = matcher(uri); - if (m.matches()) { - int i = 1; - for (String name : pathVariables) { - String val = m.group(i++); - pathParameters.put(name, val); - } - } - synchronized (vars) { - vars.put(uri, pathParameters); - } - - return pathParameters; - } - - private Matcher matcher(String uri) { - uri = filterQueryParams(uri); - Matcher m = matchers.get(uri); - if (null == m) { - m = uriPattern.matcher(uri); - synchronized (matchers) { - matchers.put(uri, m); - } - } - return m; - } - } } diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java index 948d6f573..13caa6345 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java @@ -35,6 +35,7 @@ import reactor.core.publisher.Mono; import reactor.netty.Connection; import reactor.netty.http.server.HttpServer; +import reactor.netty.http.server.WebsocketServerSpec; /** * An implementation of {@link ServerTransport} that connects to a {@link ClientTransport} via a @@ -122,8 +123,6 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { (request, response) -> { transportHeaders.get().forEach(response::addHeader); return response.sendWebsocket( - null, - FRAME_LENGTH_MASK, (in, out) -> { DuplexConnection connection = new WebsocketDuplexConnection((Connection) in); @@ -133,7 +132,10 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { connection, ByteBufAllocator.DEFAULT, mtu, false, "server"); } return acceptor.apply(connection).then(out.neverComplete()); - }); + }, + WebsocketServerSpec.builder() + .maxFramePayloadLength(FRAME_LENGTH_MASK) + .build()); }) .bind() .map(CloseableChannel::new); From 29cd7bc77adc9a8b454a48333dc128eefa315746 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 9 Apr 2020 12:36:05 +0100 Subject: [PATCH 130/181] Reduce top-level package dependencies (#770) * Reduce top-level package dependencies This commit extracts interfaces for ClientRSocketFactory, ServerRSocketFactory, and ConnectionSetupPayload, and moves the implementations into a sub-package with no references to it from the top-level package. This removes a large package cycle with io.rsocket at the center of everything since it currently contains both high level abstractions that (accessed by everything), as well as core protocol implementations classes (accessing everything). The refactoring is functionally neutral and backwards compatible. Signed-off-by: Rossen Stoyanchev * Remove deprecated methods See gh-770 Signed-off-by: Rossen Stoyanchev --- .../io/rsocket/ConnectionSetupPayload.java | 142 +--- .../main/java/io/rsocket/RSocketFactory.java | 782 +++--------------- .../core/DefaultClientRSocketFactory.java | 430 ++++++++++ .../core/DefaultConnectionSetupPayload.java | 128 +++ .../core/DefaultServerRSocketFactory.java | 379 +++++++++ .../rsocket/{ => core}/RSocketRequester.java | 5 +- .../rsocket/{ => core}/RSocketResponder.java | 6 +- .../io/rsocket/{ => core}/ReconnectMono.java | 2 +- .../rsocket/{ => core}/StreamIdSupplier.java | 2 +- .../java/io/rsocket/core/package-info.java | 24 + .../plugins/SocketAcceptorInterceptor.java | 2 +- .../{ => core}/AbstractSocketRule.java | 3 +- .../ConnectionSetupPayloadTest.java | 10 +- .../io/rsocket/{ => core}/KeepAliveTest.java | 3 +- .../rsocket/{ => core}/RSocketLeaseTest.java | 3 +- .../{ => core}/RSocketReconnectTest.java | 4 +- .../RSocketRequesterSubscribersTest.java | 3 +- .../RSocketRequesterTerminationTest.java | 6 +- .../{ => core}/RSocketRequesterTest.java | 3 +- .../{ => core}/RSocketResponderTest.java | 5 +- .../io/rsocket/{ => core}/RSocketTest.java | 5 +- .../{ => core}/ReconnectMonoTests.java | 2 +- .../{ => core}/SetupRejectionTest.java | 3 +- .../{ => core}/StreamIdSupplierTest.java | 2 +- .../io/rsocket/{ => core}/TestingStuff.java | 4 +- 25 files changed, 1141 insertions(+), 817 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java create mode 100644 rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java create mode 100644 rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java rename rsocket-core/src/main/java/io/rsocket/{ => core}/RSocketRequester.java (99%) rename rsocket-core/src/main/java/io/rsocket/{ => core}/RSocketResponder.java (99%) rename rsocket-core/src/main/java/io/rsocket/{ => core}/ReconnectMono.java (99%) rename rsocket-core/src/main/java/io/rsocket/{ => core}/StreamIdSupplier.java (98%) create mode 100644 rsocket-core/src/main/java/io/rsocket/core/package-info.java rename rsocket-core/src/test/java/io/rsocket/{ => core}/AbstractSocketRule.java (97%) rename rsocket-core/src/test/java/io/rsocket/{ => core}/ConnectionSetupPayloadTest.java (89%) rename rsocket-core/src/test/java/io/rsocket/{ => core}/KeepAliveTest.java (99%) rename rsocket-core/src/test/java/io/rsocket/{ => core}/RSocketLeaseTest.java (99%) rename rsocket-core/src/test/java/io/rsocket/{ => core}/RSocketReconnectTest.java (98%) rename rsocket-core/src/test/java/io/rsocket/{ => core}/RSocketRequesterSubscribersTest.java (98%) rename rsocket-core/src/test/java/io/rsocket/{ => core}/RSocketRequesterTerminationTest.java (93%) rename rsocket-core/src/test/java/io/rsocket/{ => core}/RSocketRequesterTest.java (99%) rename rsocket-core/src/test/java/io/rsocket/{ => core}/RSocketResponderTest.java (98%) rename rsocket-core/src/test/java/io/rsocket/{ => core}/RSocketTest.java (98%) rename rsocket-core/src/test/java/io/rsocket/{ => core}/ReconnectMonoTests.java (99%) rename rsocket-core/src/test/java/io/rsocket/{ => core}/SetupRejectionTest.java (99%) rename rsocket-core/src/test/java/io/rsocket/{ => core}/StreamIdSupplierTest.java (99%) rename rsocket-core/src/test/java/io/rsocket/{ => core}/TestingStuff.java (97%) diff --git a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java index 8762e0489..47cb0cf11 100644 --- a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java @@ -1,11 +1,11 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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 + * https://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, @@ -13,147 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.rsocket; import io.netty.buffer.ByteBuf; -import io.netty.util.AbstractReferenceCounted; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.SetupFrameFlyweight; +import io.netty.util.ReferenceCounted; import javax.annotation.Nullable; /** - * Exposed to server for determination of ResponderRSocket based on mime types and SETUP - * metadata/data + * Exposes information from the {@code SETUP} frame to a server, as well as to client responders. */ -public abstract class ConnectionSetupPayload extends AbstractReferenceCounted implements Payload { - - public static ConnectionSetupPayload create(final ByteBuf setupFrame) { - return new DefaultConnectionSetupPayload(setupFrame); - } +public interface ConnectionSetupPayload extends ReferenceCounted, Payload { - public abstract int keepAliveInterval(); + String metadataMimeType(); - public abstract int keepAliveMaxLifetime(); + String dataMimeType(); - public abstract String metadataMimeType(); + int keepAliveInterval(); - public abstract String dataMimeType(); + int keepAliveMaxLifetime(); - public abstract int getFlags(); + int getFlags(); - public abstract boolean willClientHonorLease(); + boolean willClientHonorLease(); - public abstract boolean isResumeEnabled(); + boolean isResumeEnabled(); @Nullable - public abstract ByteBuf resumeToken(); - - @Override - public ConnectionSetupPayload retain() { - super.retain(); - return this; - } - - @Override - public ConnectionSetupPayload retain(int increment) { - super.retain(increment); - return this; - } - - @Override - public abstract ConnectionSetupPayload touch(); - - @Override - public abstract ConnectionSetupPayload touch(Object hint); - - private static final class DefaultConnectionSetupPayload extends ConnectionSetupPayload { - private final ByteBuf setupFrame; - - public DefaultConnectionSetupPayload(ByteBuf setupFrame) { - this.setupFrame = setupFrame; - } - - @Override - public boolean hasMetadata() { - return FrameHeaderFlyweight.hasMetadata(setupFrame); - } - - @Override - public int keepAliveInterval() { - return SetupFrameFlyweight.keepAliveInterval(setupFrame); - } - - @Override - public int keepAliveMaxLifetime() { - return SetupFrameFlyweight.keepAliveMaxLifetime(setupFrame); - } - - @Override - public String metadataMimeType() { - return SetupFrameFlyweight.metadataMimeType(setupFrame); - } - - @Override - public String dataMimeType() { - return SetupFrameFlyweight.dataMimeType(setupFrame); - } - - @Override - public int getFlags() { - return FrameHeaderFlyweight.flags(setupFrame); - } - - @Override - public boolean willClientHonorLease() { - return SetupFrameFlyweight.honorLease(setupFrame); - } - - @Override - public boolean isResumeEnabled() { - return SetupFrameFlyweight.resumeEnabled(setupFrame); - } - - @Override - public ByteBuf resumeToken() { - return SetupFrameFlyweight.resumeToken(setupFrame); - } - - @Override - public ConnectionSetupPayload touch() { - setupFrame.touch(); - return this; - } - - @Override - public ConnectionSetupPayload touch(Object hint) { - setupFrame.touch(hint); - return this; - } - - @Override - protected void deallocate() { - setupFrame.release(); - } - - @Override - public ByteBuf sliceMetadata() { - return SetupFrameFlyweight.metadata(setupFrame); - } - - @Override - public ByteBuf sliceData() { - return SetupFrameFlyweight.data(setupFrame); - } - - @Override - public ByteBuf data() { - return sliceData(); - } - - @Override - public ByteBuf metadata() { - return sliceMetadata(); - } - } + ByteBuf resumeToken(); } diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 6570ed7a3..0d83d06dc 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -1,11 +1,11 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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 + * https://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, @@ -13,230 +13,107 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package io.rsocket; -import static io.rsocket.internal.ClientSetup.DefaultClientSetup; -import static io.rsocket.internal.ClientSetup.ResumableClientSetup; - import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.rsocket.exceptions.InvalidSetupException; -import io.rsocket.exceptions.RejectedSetupException; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.ResumeFrameFlyweight; -import io.rsocket.frame.SetupFrameFlyweight; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.ClientServerInputMultiplexer; -import io.rsocket.internal.ClientSetup; -import io.rsocket.internal.ServerSetup; -import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.lease.LeaseStats; import io.rsocket.lease.Leases; -import io.rsocket.lease.RequesterLeaseHandler; -import io.rsocket.lease.ResponderLeaseHandler; -import io.rsocket.plugins.*; -import io.rsocket.resume.*; +import io.rsocket.plugins.DuplexConnectionInterceptor; +import io.rsocket.plugins.RSocketInterceptor; +import io.rsocket.plugins.SocketAcceptorInterceptor; +import io.rsocket.resume.ResumableFramesStore; +import io.rsocket.resume.ResumeStrategy; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; -import io.rsocket.util.ConnectionUtils; -import io.rsocket.util.EmptyPayload; -import io.rsocket.util.MultiSubscriberRSocket; +import java.lang.reflect.Constructor; import java.time.Duration; -import java.util.Objects; -import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; -import reactor.core.Disposable; import reactor.core.publisher.Mono; import reactor.util.retry.Retry; -/** Factory for creating RSocket clients and servers. */ -public class RSocketFactory { +/** + * Main entry point to create RSocket clients or servers as follows: + * + *
    + *
  • {@link ClientRSocketFactory} to connect as a client. Use {@link #connect()} for a default + * instance. + *
  • {@link ServerRSocketFactory} to start a server. Use {@link #receive()} for a default + * instance. + *
+ */ +public final class RSocketFactory { + + private static final Constructor clientFactoryConstructor = + getConstructorFor("io.rsocket.core.DefaultClientRSocketFactory"); + + private static final Constructor serverFactoryConstructor = + getConstructorFor("io.rsocket.core.DefaultServerRSocketFactory"); + /** - * Creates a factory that establishes client connections to other RSockets. + * Create a {@link ClientRSocketFactory} to connect to a remote RSocket endpoint. A shortcut for + * creating {@link io.rsocket.core.DefaultClientRSocketFactory}. * - * @return a client factory + * @return the {@code ClientRSocketFactory} instance */ public static ClientRSocketFactory connect() { - return new ClientRSocketFactory(); + try { + // Avoid explicit dependency and a package cycle + return (ClientRSocketFactory) clientFactoryConstructor.newInstance(); + } catch (Exception ex) { + throw new IllegalStateException("Failed to create ClientRSocketFactory", ex); + } } /** - * Creates a factory that receives server connections from client RSockets. + * Create a {@link ServerRSocketFactory} to accept connections from RSocket clients. A shortcut + * for creating {@link io.rsocket.core.DefaultServerRSocketFactory}. * - * @return a server factory. + * @return the {@code ClientRSocketFactory} instance */ public static ServerRSocketFactory receive() { - return new ServerRSocketFactory(); - } - - public interface Start { - Mono start(); - } - - public interface ClientTransportAcceptor { - Start transport(Supplier transport); - - default Start transport(ClientTransport transport) { - return transport(() -> transport); + try { + // Avoid explicit dependency and a package cycle + return (ServerRSocketFactory) serverFactoryConstructor.newInstance(); + } catch (Exception ex) { + throw new IllegalStateException("Failed to create ServerRSocketFactory", ex); } } + /** Factory to create and configure an RSocket client, and connect to a server. */ + public interface ClientRSocketFactory extends ClientTransportAcceptor { - public interface ServerTransportAcceptor { + ClientRSocketFactory byteBufAllocator(ByteBufAllocator allocator); - ServerTransport.ConnectionAcceptor toConnectionAcceptor(); + ClientRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor interceptor); - Start transport(Supplier> transport); + ClientRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor); - default Start transport(ServerTransport transport) { - return transport(() -> transport); - } - } + ClientRSocketFactory addResponderPlugin(RSocketInterceptor interceptor); - public static class ClientRSocketFactory implements ClientTransportAcceptor { - private static final String CLIENT_TAG = "client"; + ClientRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor); - private static final BiConsumer INVALIDATE_FUNCTION = - (r, i) -> r.onClose().subscribe(null, null, i::invalidate); + ClientRSocketFactory keepAlive(Duration tickPeriod, Duration ackTimeout, int missedAcks); - private SocketAcceptor acceptor = (setup, sendingSocket) -> Mono.just(new AbstractRSocket() {}); + ClientRSocketFactory keepAliveTickPeriod(Duration tickPeriod); - private Consumer errorConsumer = Throwable::printStackTrace; - private int mtu = 0; - private PluginRegistry plugins = new PluginRegistry(Plugins.defaultPlugins()); + ClientRSocketFactory keepAliveAckTimeout(Duration ackTimeout); - private Payload setupPayload = EmptyPayload.INSTANCE; - private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; + ClientRSocketFactory keepAliveMissedAcks(int missedAcks); - private Duration tickPeriod = Duration.ofSeconds(20); - private Duration ackTimeout = Duration.ofSeconds(30); - private int missedAcks = 3; + ClientRSocketFactory mimeType(String metadataMimeType, String dataMimeType); - private String metadataMimeType = "application/binary"; - private String dataMimeType = "application/binary"; + ClientRSocketFactory dataMimeType(String dataMimeType); - private boolean resumeEnabled; - private boolean resumeCleanupStoreOnKeepAlive; - private Supplier resumeTokenSupplier = ResumeFrameFlyweight::generateResumeToken; - private Function resumeStoreFactory = - token -> new InMemoryResumableFramesStore(CLIENT_TAG, 100_000); - private Duration resumeSessionDuration = Duration.ofMinutes(2); - private Duration resumeStreamTimeout = Duration.ofSeconds(10); - private Supplier resumeStrategySupplier = - () -> - new ExponentialBackoffResumeStrategy(Duration.ofSeconds(1), Duration.ofSeconds(16), 2); + ClientRSocketFactory metadataMimeType(String metadataMimeType); - private boolean multiSubscriberRequester = true; - private boolean leaseEnabled; - private Supplier> leasesSupplier = Leases::new; - private boolean reconnectEnabled; - private Retry retrySpec; + ClientRSocketFactory lease(Supplier> leasesSupplier); - private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + ClientRSocketFactory lease(); - public ClientRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { - Objects.requireNonNull(allocator); - this.allocator = allocator; - return this; - } - - public ClientRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor interceptor) { - plugins.addConnectionPlugin(interceptor); - return this; - } - /** Deprecated. Use {@link #addRequesterPlugin(RSocketInterceptor)} instead */ - @Deprecated - public ClientRSocketFactory addClientPlugin(RSocketInterceptor interceptor) { - return addRequesterPlugin(interceptor); - } - - public ClientRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor) { - plugins.addRequesterPlugin(interceptor); - return this; - } - - /** Deprecated. Use {@link #addResponderPlugin(RSocketInterceptor)} instead */ - @Deprecated - public ClientRSocketFactory addServerPlugin(RSocketInterceptor interceptor) { - return addResponderPlugin(interceptor); - } - - public ClientRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { - plugins.addResponderPlugin(interceptor); - return this; - } - - public ClientRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor) { - plugins.addSocketAcceptorPlugin(interceptor); - return this; - } - - /** - * Deprecated as Keep-Alive is not optional according to spec - * - * @return this ClientRSocketFactory - */ - @Deprecated - public ClientRSocketFactory keepAlive() { - return this; - } - - public ClientRSocketFactory keepAlive( - Duration tickPeriod, Duration ackTimeout, int missedAcks) { - this.tickPeriod = tickPeriod; - this.ackTimeout = ackTimeout; - this.missedAcks = missedAcks; - return this; - } - - public ClientRSocketFactory keepAliveTickPeriod(Duration tickPeriod) { - this.tickPeriod = tickPeriod; - return this; - } - - public ClientRSocketFactory keepAliveAckTimeout(Duration ackTimeout) { - this.ackTimeout = ackTimeout; - return this; - } - - public ClientRSocketFactory keepAliveMissedAcks(int missedAcks) { - this.missedAcks = missedAcks; - return this; - } - - public ClientRSocketFactory mimeType(String metadataMimeType, String dataMimeType) { - this.dataMimeType = dataMimeType; - this.metadataMimeType = metadataMimeType; - return this; - } - - public ClientRSocketFactory dataMimeType(String dataMimeType) { - this.dataMimeType = dataMimeType; - return this; - } - - public ClientRSocketFactory metadataMimeType(String metadataMimeType) { - this.metadataMimeType = metadataMimeType; - return this; - } - - public ClientRSocketFactory lease(Supplier> leasesSupplier) { - this.leaseEnabled = true; - this.leasesSupplier = Objects.requireNonNull(leasesSupplier); - return this; - } - - public ClientRSocketFactory lease() { - this.leaseEnabled = true; - return this; - } - - public ClientRSocketFactory singleSubscriberRequester() { - this.multiSubscriberRequester = false; - return this; - } + ClientRSocketFactory singleSubscriberRequester(); /** * Enables a reconnectable, shared instance of {@code Mono} so every subscriber will @@ -312,531 +189,108 @@ public ClientRSocketFactory singleSubscriberRequester() { * @param retrySpec a retry factory applied for {@link Mono#retryWhen(Retry)} * @return a shared instance of {@code Mono}. */ - public ClientRSocketFactory reconnect(Retry retrySpec) { - this.retrySpec = Objects.requireNonNull(retrySpec); - this.reconnectEnabled = true; - return this; - } + ClientRSocketFactory reconnect(Retry retrySpec); - public ClientRSocketFactory resume() { - this.resumeEnabled = true; - return this; - } + ClientRSocketFactory resume(); - public ClientRSocketFactory resumeToken(Supplier resumeTokenSupplier) { - this.resumeTokenSupplier = Objects.requireNonNull(resumeTokenSupplier); - return this; - } + ClientRSocketFactory resumeToken(Supplier resumeTokenSupplier); - public ClientRSocketFactory resumeStore( - Function resumeStoreFactory) { - this.resumeStoreFactory = resumeStoreFactory; - return this; - } + ClientRSocketFactory resumeStore( + Function resumeStoreFactory); - public ClientRSocketFactory resumeSessionDuration(Duration sessionDuration) { - this.resumeSessionDuration = Objects.requireNonNull(sessionDuration); - return this; - } + ClientRSocketFactory resumeSessionDuration(Duration sessionDuration); - public ClientRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout) { - this.resumeStreamTimeout = Objects.requireNonNull(resumeStreamTimeout); - return this; - } + ClientRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout); - public ClientRSocketFactory resumeStrategy(Supplier resumeStrategy) { - this.resumeStrategySupplier = Objects.requireNonNull(resumeStrategy); - return this; - } + ClientRSocketFactory resumeStrategy(Supplier resumeStrategy); - public ClientRSocketFactory resumeCleanupOnKeepAlive() { - resumeCleanupStoreOnKeepAlive = true; - return this; - } + ClientRSocketFactory resumeCleanupOnKeepAlive(); @Override - public Start transport(Supplier transportClient) { - return new StartClient(transportClient); - } + Start transport(Supplier transportClient); - public ClientTransportAcceptor acceptor(Function acceptor) { - return acceptor(() -> acceptor); - } + ClientTransportAcceptor acceptor(Function acceptor); - public ClientTransportAcceptor acceptor(Supplier> acceptor) { - return acceptor((setup, sendingSocket) -> Mono.just(acceptor.get().apply(sendingSocket))); - } + ClientTransportAcceptor acceptor(Supplier> acceptor); - public ClientTransportAcceptor acceptor(SocketAcceptor acceptor) { - this.acceptor = acceptor; - return StartClient::new; - } + ClientTransportAcceptor acceptor(SocketAcceptor acceptor); - public ClientRSocketFactory fragment(int mtu) { - this.mtu = mtu; - return this; - } - - public ClientRSocketFactory errorConsumer(Consumer errorConsumer) { - this.errorConsumer = errorConsumer; - return this; - } + ClientRSocketFactory fragment(int mtu); - public ClientRSocketFactory setupPayload(Payload payload) { - this.setupPayload = payload; - return this; - } + ClientRSocketFactory errorConsumer(Consumer errorConsumer); - public ClientRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { - this.payloadDecoder = payloadDecoder; - return this; - } + ClientRSocketFactory setupPayload(Payload payload); - private class StartClient implements Start { - private final Supplier transportClient; - - StartClient(Supplier transportClient) { - this.transportClient = transportClient; - } - - @Override - public Mono start() { - return newConnection() - .flatMap( - connection -> { - ClientSetup clientSetup = clientSetup(connection); - ByteBuf resumeToken = clientSetup.resumeToken(); - KeepAliveHandler keepAliveHandler = clientSetup.keepAliveHandler(); - DuplexConnection wrappedConnection = clientSetup.connection(); - - ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(wrappedConnection, plugins, true); - - boolean isLeaseEnabled = leaseEnabled; - Leases leases = leasesSupplier.get(); - RequesterLeaseHandler requesterLeaseHandler = - isLeaseEnabled - ? new RequesterLeaseHandler.Impl(CLIENT_TAG, leases.receiver()) - : RequesterLeaseHandler.None; - - RSocket rSocketRequester = - new RSocketRequester( - allocator, - multiplexer.asClientConnection(), - payloadDecoder, - errorConsumer, - StreamIdSupplier.clientSupplier(), - keepAliveTickPeriod(), - keepAliveTimeout(), - keepAliveHandler, - requesterLeaseHandler); - - if (multiSubscriberRequester) { - rSocketRequester = new MultiSubscriberRSocket(rSocketRequester); - } - - RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); - - ByteBuf setupFrame = - SetupFrameFlyweight.encode( - allocator, - isLeaseEnabled, - keepAliveTickPeriod(), - keepAliveTimeout(), - resumeToken, - metadataMimeType, - dataMimeType, - setupPayload); - - ConnectionSetupPayload setup = ConnectionSetupPayload.create(setupFrame); - - return plugins - .applySocketAcceptorInterceptor(acceptor) - .accept(setup, wrappedRSocketRequester) - .flatMap( - rSocketHandler -> { - RSocket wrappedRSocketHandler = plugins.applyResponder(rSocketHandler); - - ResponderLeaseHandler responderLeaseHandler = - isLeaseEnabled - ? new ResponderLeaseHandler.Impl<>( - CLIENT_TAG, - allocator, - leases.sender(), - errorConsumer, - leases.stats()) - : ResponderLeaseHandler.None; - - RSocket rSocketResponder = - new RSocketResponder( - allocator, - multiplexer.asServerConnection(), - wrappedRSocketHandler, - payloadDecoder, - errorConsumer, - responderLeaseHandler); - - return wrappedConnection - .sendOne(setupFrame) - .thenReturn(wrappedRSocketRequester); - }); - }) - .as( - source -> { - if (reconnectEnabled) { - return new ReconnectMono<>( - source.retryWhen(retrySpec), Disposable::dispose, INVALIDATE_FUNCTION); - } else { - return source; - } - }); - } - - private int keepAliveTickPeriod() { - return (int) tickPeriod.toMillis(); - } - - private int keepAliveTimeout() { - return (int) (ackTimeout.toMillis() + tickPeriod.toMillis() * missedAcks); - } - - private ClientSetup clientSetup(DuplexConnection startConnection) { - if (resumeEnabled) { - ByteBuf resumeToken = resumeTokenSupplier.get(); - return new ResumableClientSetup( - allocator, - startConnection, - newConnection(), - resumeToken, - resumeStoreFactory.apply(resumeToken), - resumeSessionDuration, - resumeStreamTimeout, - resumeStrategySupplier, - resumeCleanupStoreOnKeepAlive); - } else { - return new DefaultClientSetup(startConnection); - } - } - - private Mono newConnection() { - return Mono.fromSupplier(transportClient).flatMap(t -> t.connect(mtu)); - } - } + ClientRSocketFactory frameDecoder(PayloadDecoder payloadDecoder); } - public static class ServerRSocketFactory { - private static final String SERVER_TAG = "server"; + /** Factory to create, configure, and start an RSocket server. */ + public interface ServerRSocketFactory { + ServerRSocketFactory byteBufAllocator(ByteBufAllocator allocator); - private SocketAcceptor acceptor; - private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; - private Consumer errorConsumer = Throwable::printStackTrace; - private int mtu = 0; - private PluginRegistry plugins = new PluginRegistry(Plugins.defaultPlugins()); + ServerRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor interceptor); - private boolean resumeSupported; - private Duration resumeSessionDuration = Duration.ofSeconds(120); - private Duration resumeStreamTimeout = Duration.ofSeconds(10); - private Function resumeStoreFactory = - token -> new InMemoryResumableFramesStore(SERVER_TAG, 100_000); + ServerRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor); - private boolean multiSubscriberRequester = true; - private boolean leaseEnabled; - private Supplier> leasesSupplier = Leases::new; + ServerRSocketFactory addResponderPlugin(RSocketInterceptor interceptor); - private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; - private boolean resumeCleanupStoreOnKeepAlive; + ServerRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor); - private ServerRSocketFactory() {} + ServerTransportAcceptor acceptor(SocketAcceptor acceptor); - public ServerRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { - Objects.requireNonNull(allocator); - this.allocator = allocator; - return this; - } + ServerRSocketFactory frameDecoder(PayloadDecoder payloadDecoder); - public ServerRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor interceptor) { - plugins.addConnectionPlugin(interceptor); - return this; - } - /** Deprecated. Use {@link #addRequesterPlugin(RSocketInterceptor)} instead */ - @Deprecated - public ServerRSocketFactory addClientPlugin(RSocketInterceptor interceptor) { - return addRequesterPlugin(interceptor); - } + ServerRSocketFactory fragment(int mtu); - public ServerRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor) { - plugins.addRequesterPlugin(interceptor); - return this; - } + ServerRSocketFactory errorConsumer(Consumer errorConsumer); - /** Deprecated. Use {@link #addResponderPlugin(RSocketInterceptor)} instead */ - @Deprecated - public ServerRSocketFactory addServerPlugin(RSocketInterceptor interceptor) { - return addResponderPlugin(interceptor); - } + ServerRSocketFactory lease(Supplier> leasesSupplier); - public ServerRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { - plugins.addResponderPlugin(interceptor); - return this; - } + ServerRSocketFactory lease(); - public ServerRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor) { - plugins.addSocketAcceptorPlugin(interceptor); - return this; - } + ServerRSocketFactory singleSubscriberRequester(); - public ServerTransportAcceptor acceptor(SocketAcceptor acceptor) { - this.acceptor = acceptor; - return new ServerStart<>(); - } + ServerRSocketFactory resume(); - public ServerRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { - this.payloadDecoder = payloadDecoder; - return this; - } + ServerRSocketFactory resumeStore( + Function resumeStoreFactory); - public ServerRSocketFactory fragment(int mtu) { - this.mtu = mtu; - return this; - } + ServerRSocketFactory resumeSessionDuration(Duration sessionDuration); - public ServerRSocketFactory errorConsumer(Consumer errorConsumer) { - this.errorConsumer = errorConsumer; - return this; - } + ServerRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout); - public ServerRSocketFactory lease(Supplier> leasesSupplier) { - this.leaseEnabled = true; - this.leasesSupplier = Objects.requireNonNull(leasesSupplier); - return this; - } + ServerRSocketFactory resumeCleanupOnKeepAlive(); + } - public ServerRSocketFactory lease() { - this.leaseEnabled = true; - return this; - } + public interface ClientTransportAcceptor { + Start transport(Supplier transport); - public ServerRSocketFactory singleSubscriberRequester() { - this.multiSubscriberRequester = false; - return this; + default Start transport(ClientTransport transport) { + return transport(() -> transport); } + } - public ServerRSocketFactory resume() { - this.resumeSupported = true; - return this; - } + public interface ServerTransportAcceptor { - public ServerRSocketFactory resumeStore( - Function resumeStoreFactory) { - this.resumeStoreFactory = resumeStoreFactory; - return this; - } + ServerTransport.ConnectionAcceptor toConnectionAcceptor(); - public ServerRSocketFactory resumeSessionDuration(Duration sessionDuration) { - this.resumeSessionDuration = Objects.requireNonNull(sessionDuration); - return this; - } + Start transport(Supplier> transport); - public ServerRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout) { - this.resumeStreamTimeout = Objects.requireNonNull(resumeStreamTimeout); - return this; + default Start transport(ServerTransport transport) { + return transport(() -> transport); } + } - public ServerRSocketFactory resumeCleanupOnKeepAlive() { - resumeCleanupStoreOnKeepAlive = true; - return this; - } + public interface Start { + Mono start(); + } - private class ServerStart implements Start, ServerTransportAcceptor { - private Supplier> transportServer; - - @Override - public ServerTransport.ConnectionAcceptor toConnectionAcceptor() { - return new ServerTransport.ConnectionAcceptor() { - private final ServerSetup serverSetup = serverSetup(); - - @Override - public Mono apply(DuplexConnection connection) { - return acceptor(serverSetup, connection); - } - }; - } - - @Override - @SuppressWarnings("unchecked") - public Start transport(Supplier> transport) { - this.transportServer = (Supplier) transport; - return (Start) this::start; - } - - private Mono acceptor(ServerSetup serverSetup, DuplexConnection connection) { - ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(connection, plugins, false); - - return multiplexer - .asSetupConnection() - .receive() - .next() - .flatMap(startFrame -> accept(serverSetup, startFrame, multiplexer)); - } - - private Mono acceptResume( - ServerSetup serverSetup, ByteBuf resumeFrame, ClientServerInputMultiplexer multiplexer) { - return serverSetup.acceptRSocketResume(resumeFrame, multiplexer); - } - - private Mono accept( - ServerSetup serverSetup, ByteBuf startFrame, ClientServerInputMultiplexer multiplexer) { - switch (FrameHeaderFlyweight.frameType(startFrame)) { - case SETUP: - return acceptSetup(serverSetup, startFrame, multiplexer); - case RESUME: - return acceptResume(serverSetup, startFrame, multiplexer); - default: - return acceptUnknown(startFrame, multiplexer); - } - } - - private Mono acceptSetup( - ServerSetup serverSetup, ByteBuf setupFrame, ClientServerInputMultiplexer multiplexer) { - - if (!SetupFrameFlyweight.isSupportedVersion(setupFrame)) { - return sendError( - multiplexer, - new InvalidSetupException( - "Unsupported version: " - + SetupFrameFlyweight.humanReadableVersion(setupFrame))) - .doFinally( - signalType -> { - setupFrame.release(); - multiplexer.dispose(); - }); - } - - boolean isLeaseEnabled = leaseEnabled; - - if (SetupFrameFlyweight.honorLease(setupFrame) && !isLeaseEnabled) { - return sendError(multiplexer, new InvalidSetupException("lease is not supported")) - .doFinally( - signalType -> { - setupFrame.release(); - multiplexer.dispose(); - }); - } - - return serverSetup.acceptRSocketSetup( - setupFrame, - multiplexer, - (keepAliveHandler, wrappedMultiplexer) -> { - ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(setupFrame); - - Leases leases = leasesSupplier.get(); - RequesterLeaseHandler requesterLeaseHandler = - isLeaseEnabled - ? new RequesterLeaseHandler.Impl(SERVER_TAG, leases.receiver()) - : RequesterLeaseHandler.None; - - RSocket rSocketRequester = - new RSocketRequester( - allocator, - wrappedMultiplexer.asServerConnection(), - payloadDecoder, - errorConsumer, - StreamIdSupplier.serverSupplier(), - setupPayload.keepAliveInterval(), - setupPayload.keepAliveMaxLifetime(), - keepAliveHandler, - requesterLeaseHandler); - - if (multiSubscriberRequester) { - rSocketRequester = new MultiSubscriberRSocket(rSocketRequester); - } - RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); - - return plugins - .applySocketAcceptorInterceptor(acceptor) - .accept(setupPayload, wrappedRSocketRequester) - .onErrorResume( - err -> sendError(multiplexer, rejectedSetupError(err)).then(Mono.error(err))) - .doOnNext( - rSocketHandler -> { - RSocket wrappedRSocketHandler = plugins.applyResponder(rSocketHandler); - - ResponderLeaseHandler responderLeaseHandler = - isLeaseEnabled - ? new ResponderLeaseHandler.Impl<>( - SERVER_TAG, - allocator, - leases.sender(), - errorConsumer, - leases.stats()) - : ResponderLeaseHandler.None; - - RSocket rSocketResponder = - new RSocketResponder( - allocator, - wrappedMultiplexer.asClientConnection(), - wrappedRSocketHandler, - payloadDecoder, - errorConsumer, - responderLeaseHandler); - }) - .doFinally(signalType -> setupPayload.release()) - .then(); - }); - } - - @Override - public Mono start() { - return Mono.defer( - new Supplier>() { - - ServerSetup serverSetup = serverSetup(); - - @Override - public Mono get() { - return Mono.fromSupplier(transportServer) - .flatMap( - transport -> - transport.start( - duplexConnection -> acceptor(serverSetup, duplexConnection), mtu)) - .doOnNext(c -> c.onClose().doFinally(v -> serverSetup.dispose()).subscribe()); - } - }); - } - - private ServerSetup serverSetup() { - return resumeSupported - ? new ServerSetup.ResumableServerSetup( - allocator, - new SessionManager(), - resumeSessionDuration, - resumeStreamTimeout, - resumeStoreFactory, - resumeCleanupStoreOnKeepAlive) - : new ServerSetup.DefaultServerSetup(allocator); - } - - private Mono acceptUnknown(ByteBuf frame, ClientServerInputMultiplexer multiplexer) { - return sendError( - multiplexer, - new InvalidSetupException( - "invalid setup frame: " + FrameHeaderFlyweight.frameType(frame))) - .doFinally( - signalType -> { - frame.release(); - multiplexer.dispose(); - }); - } - - private Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { - return ConnectionUtils.sendError(allocator, multiplexer, exception); - } - - private Exception rejectedSetupError(Throwable err) { - String msg = err.getMessage(); - return new RejectedSetupException(msg == null ? "rejected by server acceptor" : msg); - } + private static Constructor getConstructorFor(String className) { + try { + Class clazz = Class.forName(className); + return clazz.getDeclaredConstructor(); + } catch (Throwable ex) { + throw new IllegalStateException("No " + className); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java new file mode 100644 index 000000000..ce43cd1fd --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java @@ -0,0 +1,430 @@ +/* + * 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 + * + * https://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.core; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.AbstractRSocket; +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.DuplexConnection; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.RSocketFactory; +import io.rsocket.SocketAcceptor; +import io.rsocket.frame.ResumeFrameFlyweight; +import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.ClientServerInputMultiplexer; +import io.rsocket.internal.ClientSetup; +import io.rsocket.keepalive.KeepAliveHandler; +import io.rsocket.lease.LeaseStats; +import io.rsocket.lease.Leases; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.plugins.DuplexConnectionInterceptor; +import io.rsocket.plugins.PluginRegistry; +import io.rsocket.plugins.Plugins; +import io.rsocket.plugins.RSocketInterceptor; +import io.rsocket.plugins.SocketAcceptorInterceptor; +import io.rsocket.resume.ExponentialBackoffResumeStrategy; +import io.rsocket.resume.InMemoryResumableFramesStore; +import io.rsocket.resume.ResumableFramesStore; +import io.rsocket.resume.ResumeStrategy; +import io.rsocket.transport.ClientTransport; +import io.rsocket.util.EmptyPayload; +import io.rsocket.util.MultiSubscriberRSocket; +import java.time.Duration; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.Disposable; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +/** + * Default implementation of {@link RSocketFactory.ClientRSocketFactory} that can be instantiated + * directly or through the shortcut {@link RSocketFactory#connect()}. + */ +public class DefaultClientRSocketFactory implements RSocketFactory.ClientRSocketFactory { + private static final String CLIENT_TAG = "client"; + + private static final BiConsumer INVALIDATE_FUNCTION = + (r, i) -> r.onClose().subscribe(null, null, i::invalidate); + + private SocketAcceptor acceptor = (setup, sendingSocket) -> Mono.just(new AbstractRSocket() {}); + + private Consumer errorConsumer = Throwable::printStackTrace; + private int mtu = 0; + private PluginRegistry plugins = new PluginRegistry(Plugins.defaultPlugins()); + + private Payload setupPayload = EmptyPayload.INSTANCE; + private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; + + private Duration tickPeriod = Duration.ofSeconds(20); + private Duration ackTimeout = Duration.ofSeconds(30); + private int missedAcks = 3; + + private String metadataMimeType = "application/binary"; + private String dataMimeType = "application/binary"; + + private boolean resumeEnabled; + private boolean resumeCleanupStoreOnKeepAlive; + private Supplier resumeTokenSupplier = ResumeFrameFlyweight::generateResumeToken; + private Function resumeStoreFactory = + token -> new InMemoryResumableFramesStore(CLIENT_TAG, 100_000); + private Duration resumeSessionDuration = Duration.ofMinutes(2); + private Duration resumeStreamTimeout = Duration.ofSeconds(10); + private Supplier resumeStrategySupplier = + () -> new ExponentialBackoffResumeStrategy(Duration.ofSeconds(1), Duration.ofSeconds(16), 2); + + private boolean multiSubscriberRequester = true; + private boolean leaseEnabled; + private Supplier> leasesSupplier = Leases::new; + private boolean reconnectEnabled; + private Retry retrySpec; + + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + + @Override + public RSocketFactory.ClientRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { + Objects.requireNonNull(allocator); + this.allocator = allocator; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory addConnectionPlugin( + DuplexConnectionInterceptor interceptor) { + plugins.addConnectionPlugin(interceptor); + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor) { + plugins.addRequesterPlugin(interceptor); + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { + plugins.addResponderPlugin(interceptor); + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory addSocketAcceptorPlugin( + SocketAcceptorInterceptor interceptor) { + plugins.addSocketAcceptorPlugin(interceptor); + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory keepAlive( + Duration tickPeriod, Duration ackTimeout, int missedAcks) { + this.tickPeriod = tickPeriod; + this.ackTimeout = ackTimeout; + this.missedAcks = missedAcks; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory keepAliveTickPeriod(Duration tickPeriod) { + this.tickPeriod = tickPeriod; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory keepAliveAckTimeout(Duration ackTimeout) { + this.ackTimeout = ackTimeout; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory keepAliveMissedAcks(int missedAcks) { + this.missedAcks = missedAcks; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory mimeType( + String metadataMimeType, String dataMimeType) { + this.dataMimeType = dataMimeType; + this.metadataMimeType = metadataMimeType; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory dataMimeType(String dataMimeType) { + this.dataMimeType = dataMimeType; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory metadataMimeType(String metadataMimeType) { + this.metadataMimeType = metadataMimeType; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory lease( + Supplier> leasesSupplier) { + this.leaseEnabled = true; + this.leasesSupplier = Objects.requireNonNull(leasesSupplier); + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory lease() { + this.leaseEnabled = true; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory singleSubscriberRequester() { + this.multiSubscriberRequester = false; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory reconnect(Retry retrySpec) { + this.retrySpec = Objects.requireNonNull(retrySpec); + this.reconnectEnabled = true; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory resume() { + this.resumeEnabled = true; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory resumeToken(Supplier resumeTokenSupplier) { + this.resumeTokenSupplier = Objects.requireNonNull(resumeTokenSupplier); + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory resumeStore( + Function resumeStoreFactory) { + this.resumeStoreFactory = resumeStoreFactory; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory resumeSessionDuration(Duration sessionDuration) { + this.resumeSessionDuration = Objects.requireNonNull(sessionDuration); + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout) { + this.resumeStreamTimeout = Objects.requireNonNull(resumeStreamTimeout); + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory resumeStrategy( + Supplier resumeStrategy) { + this.resumeStrategySupplier = Objects.requireNonNull(resumeStrategy); + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory resumeCleanupOnKeepAlive() { + resumeCleanupStoreOnKeepAlive = true; + return this; + } + + @Override + public RSocketFactory.Start transport(Supplier transportClient) { + return new StartClient(transportClient); + } + + @Override + public RSocketFactory.ClientTransportAcceptor acceptor(Function acceptor) { + return acceptor(() -> acceptor); + } + + @Override + public RSocketFactory.ClientTransportAcceptor acceptor( + Supplier> acceptor) { + return acceptor((setup, sendingSocket) -> Mono.just(acceptor.get().apply(sendingSocket))); + } + + @Override + public RSocketFactory.ClientTransportAcceptor acceptor(SocketAcceptor acceptor) { + this.acceptor = acceptor; + return StartClient::new; + } + + @Override + public RSocketFactory.ClientRSocketFactory fragment(int mtu) { + this.mtu = mtu; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory errorConsumer(Consumer errorConsumer) { + this.errorConsumer = errorConsumer; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory setupPayload(Payload payload) { + this.setupPayload = payload; + return this; + } + + @Override + public RSocketFactory.ClientRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { + this.payloadDecoder = payloadDecoder; + return this; + } + + private class StartClient implements RSocketFactory.Start { + private final Supplier transportClient; + + StartClient(Supplier transportClient) { + this.transportClient = transportClient; + } + + @Override + public Mono start() { + return newConnection() + .flatMap( + connection -> { + ClientSetup clientSetup = clientSetup(connection); + ByteBuf resumeToken = clientSetup.resumeToken(); + KeepAliveHandler keepAliveHandler = clientSetup.keepAliveHandler(); + DuplexConnection wrappedConnection = clientSetup.connection(); + + ClientServerInputMultiplexer multiplexer = + new ClientServerInputMultiplexer(wrappedConnection, plugins, true); + + boolean isLeaseEnabled = leaseEnabled; + Leases leases = leasesSupplier.get(); + RequesterLeaseHandler requesterLeaseHandler = + isLeaseEnabled + ? new RequesterLeaseHandler.Impl(CLIENT_TAG, leases.receiver()) + : RequesterLeaseHandler.None; + + RSocket rSocketRequester = + new RSocketRequester( + allocator, + multiplexer.asClientConnection(), + payloadDecoder, + errorConsumer, + StreamIdSupplier.clientSupplier(), + keepAliveTickPeriod(), + keepAliveTimeout(), + keepAliveHandler, + requesterLeaseHandler); + + if (multiSubscriberRequester) { + rSocketRequester = new MultiSubscriberRSocket(rSocketRequester); + } + + RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); + + ByteBuf setupFrame = + SetupFrameFlyweight.encode( + allocator, + isLeaseEnabled, + keepAliveTickPeriod(), + keepAliveTimeout(), + resumeToken, + metadataMimeType, + dataMimeType, + setupPayload); + + ConnectionSetupPayload setup = new DefaultConnectionSetupPayload(setupFrame); + + return plugins + .applySocketAcceptorInterceptor(acceptor) + .accept(setup, wrappedRSocketRequester) + .flatMap( + rSocketHandler -> { + RSocket wrappedRSocketHandler = plugins.applyResponder(rSocketHandler); + + ResponderLeaseHandler responderLeaseHandler = + isLeaseEnabled + ? new ResponderLeaseHandler.Impl<>( + CLIENT_TAG, + allocator, + leases.sender(), + errorConsumer, + leases.stats()) + : ResponderLeaseHandler.None; + + RSocket rSocketResponder = + new RSocketResponder( + allocator, + multiplexer.asServerConnection(), + wrappedRSocketHandler, + payloadDecoder, + errorConsumer, + responderLeaseHandler); + + return wrappedConnection + .sendOne(setupFrame) + .thenReturn(wrappedRSocketRequester); + }); + }) + .as( + source -> { + if (reconnectEnabled) { + return new ReconnectMono<>( + source.retryWhen(retrySpec), Disposable::dispose, INVALIDATE_FUNCTION); + } else { + return source; + } + }); + } + + private int keepAliveTickPeriod() { + return (int) tickPeriod.toMillis(); + } + + private int keepAliveTimeout() { + return (int) (ackTimeout.toMillis() + tickPeriod.toMillis() * missedAcks); + } + + private ClientSetup clientSetup(DuplexConnection startConnection) { + if (resumeEnabled) { + ByteBuf resumeToken = resumeTokenSupplier.get(); + return new ClientSetup.ResumableClientSetup( + allocator, + startConnection, + newConnection(), + resumeToken, + resumeStoreFactory.apply(resumeToken), + resumeSessionDuration, + resumeStreamTimeout, + resumeStrategySupplier, + resumeCleanupStoreOnKeepAlive); + } else { + return new ClientSetup.DefaultClientSetup(startConnection); + } + } + + private Mono newConnection() { + return Mono.fromSupplier(transportClient).flatMap(t -> t.connect(mtu)); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java new file mode 100644 index 000000000..23eeac160 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java @@ -0,0 +1,128 @@ +/* + * 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.core; + +import io.netty.buffer.ByteBuf; +import io.netty.util.AbstractReferenceCounted; +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.SetupFrameFlyweight; + +/** Default implementation of {@link ConnectionSetupPayload}. */ +class DefaultConnectionSetupPayload extends AbstractReferenceCounted + implements ConnectionSetupPayload { + + private final ByteBuf setupFrame; + + public DefaultConnectionSetupPayload(ByteBuf setupFrame) { + this.setupFrame = setupFrame; + } + + @Override + public ConnectionSetupPayload retain() { + super.retain(); + return this; + } + + @Override + public ConnectionSetupPayload retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public boolean hasMetadata() { + return FrameHeaderFlyweight.hasMetadata(setupFrame); + } + + @Override + public int keepAliveInterval() { + return SetupFrameFlyweight.keepAliveInterval(setupFrame); + } + + @Override + public int keepAliveMaxLifetime() { + return SetupFrameFlyweight.keepAliveMaxLifetime(setupFrame); + } + + @Override + public String metadataMimeType() { + return SetupFrameFlyweight.metadataMimeType(setupFrame); + } + + @Override + public String dataMimeType() { + return SetupFrameFlyweight.dataMimeType(setupFrame); + } + + @Override + public int getFlags() { + return FrameHeaderFlyweight.flags(setupFrame); + } + + @Override + public boolean willClientHonorLease() { + return SetupFrameFlyweight.honorLease(setupFrame); + } + + @Override + public boolean isResumeEnabled() { + return SetupFrameFlyweight.resumeEnabled(setupFrame); + } + + @Override + public ByteBuf resumeToken() { + return SetupFrameFlyweight.resumeToken(setupFrame); + } + + @Override + public ConnectionSetupPayload touch() { + setupFrame.touch(); + return this; + } + + @Override + public ConnectionSetupPayload touch(Object hint) { + setupFrame.touch(hint); + return this; + } + + @Override + protected void deallocate() { + setupFrame.release(); + } + + @Override + public ByteBuf sliceMetadata() { + return SetupFrameFlyweight.metadata(setupFrame); + } + + @Override + public ByteBuf sliceData() { + return SetupFrameFlyweight.data(setupFrame); + } + + @Override + public ByteBuf data() { + return sliceData(); + } + + @Override + public ByteBuf metadata() { + return sliceMetadata(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java new file mode 100644 index 000000000..f2acb9af0 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java @@ -0,0 +1,379 @@ +/* + * 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 + * + * https://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.core; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Closeable; +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.DuplexConnection; +import io.rsocket.RSocket; +import io.rsocket.RSocketFactory; +import io.rsocket.SocketAcceptor; +import io.rsocket.exceptions.InvalidSetupException; +import io.rsocket.exceptions.RejectedSetupException; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.ClientServerInputMultiplexer; +import io.rsocket.internal.ServerSetup; +import io.rsocket.lease.Leases; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.plugins.DuplexConnectionInterceptor; +import io.rsocket.plugins.PluginRegistry; +import io.rsocket.plugins.Plugins; +import io.rsocket.plugins.RSocketInterceptor; +import io.rsocket.plugins.SocketAcceptorInterceptor; +import io.rsocket.resume.InMemoryResumableFramesStore; +import io.rsocket.resume.ResumableFramesStore; +import io.rsocket.resume.SessionManager; +import io.rsocket.transport.ServerTransport; +import io.rsocket.util.ConnectionUtils; +import io.rsocket.util.MultiSubscriberRSocket; +import java.time.Duration; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +/** + * Default implementation of {@link RSocketFactory.ServerRSocketFactory} that can be instantiated + * directly or through the shortcut {@link RSocketFactory#receive()}. + */ +public class DefaultServerRSocketFactory implements RSocketFactory.ServerRSocketFactory { + private static final String SERVER_TAG = "server"; + + private SocketAcceptor acceptor; + private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; + private Consumer errorConsumer = Throwable::printStackTrace; + private int mtu = 0; + private PluginRegistry plugins = new PluginRegistry(Plugins.defaultPlugins()); + + private boolean resumeSupported; + private Duration resumeSessionDuration = Duration.ofSeconds(120); + private Duration resumeStreamTimeout = Duration.ofSeconds(10); + private Function resumeStoreFactory = + token -> new InMemoryResumableFramesStore(SERVER_TAG, 100_000); + + private boolean multiSubscriberRequester = true; + private boolean leaseEnabled; + private Supplier> leasesSupplier = Leases::new; + + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + private boolean resumeCleanupStoreOnKeepAlive; + + @Override + public RSocketFactory.ServerRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { + Objects.requireNonNull(allocator); + this.allocator = allocator; + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory addConnectionPlugin( + DuplexConnectionInterceptor interceptor) { + plugins.addConnectionPlugin(interceptor); + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor) { + plugins.addRequesterPlugin(interceptor); + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { + plugins.addResponderPlugin(interceptor); + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory addSocketAcceptorPlugin( + SocketAcceptorInterceptor interceptor) { + plugins.addSocketAcceptorPlugin(interceptor); + return this; + } + + @Override + public RSocketFactory.ServerTransportAcceptor acceptor(SocketAcceptor acceptor) { + this.acceptor = acceptor; + return new ServerStart<>(); + } + + @Override + public RSocketFactory.ServerRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { + this.payloadDecoder = payloadDecoder; + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory fragment(int mtu) { + this.mtu = mtu; + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory errorConsumer(Consumer errorConsumer) { + this.errorConsumer = errorConsumer; + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory lease(Supplier> leasesSupplier) { + this.leaseEnabled = true; + this.leasesSupplier = Objects.requireNonNull(leasesSupplier); + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory lease() { + this.leaseEnabled = true; + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory singleSubscriberRequester() { + this.multiSubscriberRequester = false; + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory resume() { + this.resumeSupported = true; + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory resumeStore( + Function resumeStoreFactory) { + this.resumeStoreFactory = resumeStoreFactory; + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory resumeSessionDuration(Duration sessionDuration) { + this.resumeSessionDuration = Objects.requireNonNull(sessionDuration); + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout) { + this.resumeStreamTimeout = Objects.requireNonNull(resumeStreamTimeout); + return this; + } + + @Override + public RSocketFactory.ServerRSocketFactory resumeCleanupOnKeepAlive() { + resumeCleanupStoreOnKeepAlive = true; + return this; + } + + private class ServerStart + implements RSocketFactory.Start, RSocketFactory.ServerTransportAcceptor { + private Supplier> transportServer; + + @Override + public ServerTransport.ConnectionAcceptor toConnectionAcceptor() { + return new ServerTransport.ConnectionAcceptor() { + private final ServerSetup serverSetup = serverSetup(); + + @Override + public Mono apply(DuplexConnection connection) { + return acceptor(serverSetup, connection); + } + }; + } + + @Override + @SuppressWarnings("unchecked") + public RSocketFactory.Start transport( + Supplier> transport) { + this.transportServer = (Supplier) transport; + return (RSocketFactory.Start) this::start; + } + + private Mono acceptor(ServerSetup serverSetup, DuplexConnection connection) { + ClientServerInputMultiplexer multiplexer = + new ClientServerInputMultiplexer(connection, plugins, false); + + return multiplexer + .asSetupConnection() + .receive() + .next() + .flatMap(startFrame -> accept(serverSetup, startFrame, multiplexer)); + } + + private Mono acceptResume( + ServerSetup serverSetup, ByteBuf resumeFrame, ClientServerInputMultiplexer multiplexer) { + return serverSetup.acceptRSocketResume(resumeFrame, multiplexer); + } + + private Mono accept( + ServerSetup serverSetup, ByteBuf startFrame, ClientServerInputMultiplexer multiplexer) { + switch (FrameHeaderFlyweight.frameType(startFrame)) { + case SETUP: + return acceptSetup(serverSetup, startFrame, multiplexer); + case RESUME: + return acceptResume(serverSetup, startFrame, multiplexer); + default: + return acceptUnknown(startFrame, multiplexer); + } + } + + private Mono acceptSetup( + ServerSetup serverSetup, ByteBuf setupFrame, ClientServerInputMultiplexer multiplexer) { + + if (!SetupFrameFlyweight.isSupportedVersion(setupFrame)) { + return sendError( + multiplexer, + new InvalidSetupException( + "Unsupported version: " + SetupFrameFlyweight.humanReadableVersion(setupFrame))) + .doFinally( + signalType -> { + setupFrame.release(); + multiplexer.dispose(); + }); + } + + boolean isLeaseEnabled = leaseEnabled; + + if (SetupFrameFlyweight.honorLease(setupFrame) && !isLeaseEnabled) { + return sendError(multiplexer, new InvalidSetupException("lease is not supported")) + .doFinally( + signalType -> { + setupFrame.release(); + multiplexer.dispose(); + }); + } + + return serverSetup.acceptRSocketSetup( + setupFrame, + multiplexer, + (keepAliveHandler, wrappedMultiplexer) -> { + ConnectionSetupPayload setupPayload = new DefaultConnectionSetupPayload(setupFrame); + + Leases leases = leasesSupplier.get(); + RequesterLeaseHandler requesterLeaseHandler = + isLeaseEnabled + ? new RequesterLeaseHandler.Impl(SERVER_TAG, leases.receiver()) + : RequesterLeaseHandler.None; + + RSocket rSocketRequester = + new RSocketRequester( + allocator, + wrappedMultiplexer.asServerConnection(), + payloadDecoder, + errorConsumer, + StreamIdSupplier.serverSupplier(), + setupPayload.keepAliveInterval(), + setupPayload.keepAliveMaxLifetime(), + keepAliveHandler, + requesterLeaseHandler); + + if (multiSubscriberRequester) { + rSocketRequester = new MultiSubscriberRSocket(rSocketRequester); + } + RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); + + return plugins + .applySocketAcceptorInterceptor(acceptor) + .accept(setupPayload, wrappedRSocketRequester) + .onErrorResume( + err -> sendError(multiplexer, rejectedSetupError(err)).then(Mono.error(err))) + .doOnNext( + rSocketHandler -> { + RSocket wrappedRSocketHandler = plugins.applyResponder(rSocketHandler); + + ResponderLeaseHandler responderLeaseHandler = + isLeaseEnabled + ? new ResponderLeaseHandler.Impl<>( + SERVER_TAG, + allocator, + leases.sender(), + errorConsumer, + leases.stats()) + : ResponderLeaseHandler.None; + + RSocket rSocketResponder = + new RSocketResponder( + allocator, + wrappedMultiplexer.asClientConnection(), + wrappedRSocketHandler, + payloadDecoder, + errorConsumer, + responderLeaseHandler); + }) + .doFinally(signalType -> setupPayload.release()) + .then(); + }); + } + + @Override + public Mono start() { + return Mono.defer( + new Supplier>() { + + ServerSetup serverSetup = serverSetup(); + + @Override + public Mono get() { + return Mono.fromSupplier(transportServer) + .flatMap( + transport -> + transport.start( + duplexConnection -> acceptor(serverSetup, duplexConnection), mtu)) + .doOnNext(c -> c.onClose().doFinally(v -> serverSetup.dispose()).subscribe()); + } + }); + } + + private ServerSetup serverSetup() { + return resumeSupported + ? new ServerSetup.ResumableServerSetup( + allocator, + new SessionManager(), + resumeSessionDuration, + resumeStreamTimeout, + resumeStoreFactory, + resumeCleanupStoreOnKeepAlive) + : new ServerSetup.DefaultServerSetup(allocator); + } + + private Mono acceptUnknown(ByteBuf frame, ClientServerInputMultiplexer multiplexer) { + return sendError( + multiplexer, + new InvalidSetupException( + "invalid setup frame: " + FrameHeaderFlyweight.frameType(frame))) + .doFinally( + signalType -> { + frame.release(); + multiplexer.dispose(); + }); + } + + private Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { + return ConnectionUtils.sendError(allocator, multiplexer, exception); + } + + private Exception rejectedSetupError(Throwable err) { + String msg = err.getMessage(); + return new RejectedSetupException(msg == null ? "rejected by server acceptor" : msg); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java similarity index 99% rename from rsocket-core/src/main/java/io/rsocket/RSocketRequester.java rename to rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index ed13e9f6e..e39fa7a29 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import static io.rsocket.keepalive.KeepAliveSupport.ClientKeepAliveSupport; import static io.rsocket.keepalive.KeepAliveSupport.KeepAlive; @@ -23,6 +23,9 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.util.ReferenceCountUtil; import io.netty.util.collection.IntObjectMap; +import io.rsocket.DuplexConnection; +import io.rsocket.Payload; +import io.rsocket.RSocket; import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.exceptions.Exceptions; import io.rsocket.frame.CancelFrameFlyweight; diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java similarity index 99% rename from rsocket-core/src/main/java/io/rsocket/RSocketResponder.java rename to rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index 53ced9763..d742300d1 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -14,12 +14,16 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.ReferenceCountUtil; import io.netty.util.collection.IntObjectMap; +import io.rsocket.DuplexConnection; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.ResponderRSocket; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.frame.*; import io.rsocket.frame.decoder.PayloadDecoder; diff --git a/rsocket-core/src/main/java/io/rsocket/ReconnectMono.java b/rsocket-core/src/main/java/io/rsocket/core/ReconnectMono.java similarity index 99% rename from rsocket-core/src/main/java/io/rsocket/ReconnectMono.java rename to rsocket-core/src/main/java/io/rsocket/core/ReconnectMono.java index b2c36908d..81f6625f0 100644 --- a/rsocket-core/src/main/java/io/rsocket/ReconnectMono.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ReconnectMono.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import java.time.Duration; import java.util.concurrent.CancellationException; diff --git a/rsocket-core/src/main/java/io/rsocket/StreamIdSupplier.java b/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java similarity index 98% rename from rsocket-core/src/main/java/io/rsocket/StreamIdSupplier.java rename to rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java index af8c6b3d0..70734b8c0 100644 --- a/rsocket-core/src/main/java/io/rsocket/StreamIdSupplier.java +++ b/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import io.netty.util.collection.IntObjectMap; import java.util.concurrent.atomic.AtomicLongFieldUpdater; diff --git a/rsocket-core/src/main/java/io/rsocket/core/package-info.java b/rsocket-core/src/main/java/io/rsocket/core/package-info.java new file mode 100644 index 000000000..a70bb3b16 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/package-info.java @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** + * Contains core RSocket protocol, client and server implementation classes, including factories to + * create and configure them. + */ +@NonNullApi +package io.rsocket.core; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java index c9201ca5b..0cb9d92d2 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2015-2019 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. diff --git a/rsocket-core/src/test/java/io/rsocket/AbstractSocketRule.java b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java similarity index 97% rename from rsocket-core/src/test/java/io/rsocket/AbstractSocketRule.java rename to rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java index 22568bfcc..dc01e7911 100644 --- a/rsocket-core/src/test/java/io/rsocket/AbstractSocketRule.java +++ b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java @@ -14,8 +14,9 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; +import io.rsocket.RSocket; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestSubscriber; import java.util.concurrent.ConcurrentLinkedQueue; diff --git a/rsocket-core/src/test/java/io/rsocket/ConnectionSetupPayloadTest.java b/rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java similarity index 89% rename from rsocket-core/src/test/java/io/rsocket/ConnectionSetupPayloadTest.java rename to rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java index 16e0f2ec7..ea3142d25 100644 --- a/rsocket-core/src/test/java/io/rsocket/ConnectionSetupPayloadTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java @@ -1,4 +1,4 @@ -package io.rsocket; +package io.rsocket.core; import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -6,6 +6,8 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.Payload; import io.rsocket.frame.SetupFrameFlyweight; import io.rsocket.util.DefaultPayload; import org.junit.jupiter.api.Test; @@ -24,7 +26,7 @@ void testSetupPayloadWithDataMetadata() { boolean leaseEnabled = true; ByteBuf frame = encodeSetupFrame(leaseEnabled, payload); - ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(frame); + ConnectionSetupPayload setupPayload = new DefaultConnectionSetupPayload(frame); assertTrue(setupPayload.willClientHonorLease()); assertEquals(KEEP_ALIVE_INTERVAL, setupPayload.keepAliveInterval()); @@ -46,7 +48,7 @@ void testSetupPayloadWithNoMetadata() { boolean leaseEnabled = false; ByteBuf frame = encodeSetupFrame(leaseEnabled, payload); - ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(frame); + ConnectionSetupPayload setupPayload = new DefaultConnectionSetupPayload(frame); assertFalse(setupPayload.willClientHonorLease()); assertFalse(setupPayload.hasMetadata()); @@ -64,7 +66,7 @@ void testSetupPayloadWithEmptyMetadata() { boolean leaseEnabled = false; ByteBuf frame = encodeSetupFrame(leaseEnabled, payload); - ConnectionSetupPayload setupPayload = ConnectionSetupPayload.create(frame); + ConnectionSetupPayload setupPayload = new DefaultConnectionSetupPayload(frame); assertFalse(setupPayload.willClientHonorLease()); assertTrue(setupPayload.hasMetadata()); diff --git a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java similarity index 99% rename from rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java rename to rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java index b275ccc33..6cb05dec1 100644 --- a/rsocket-core/src/test/java/io/rsocket/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import static io.rsocket.keepalive.KeepAliveHandler.DefaultKeepAliveHandler; import static io.rsocket.keepalive.KeepAliveHandler.ResumableKeepAliveHandler; @@ -22,6 +22,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.rsocket.RSocket; import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java similarity index 99% rename from rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java rename to rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java index 3af8916cd..3cbb3c5d7 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import static io.rsocket.frame.FrameType.ERROR; import static io.rsocket.frame.FrameType.SETUP; @@ -25,6 +25,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.buffer.UnpooledByteBufAllocator; +import io.rsocket.*; import io.rsocket.exceptions.Exceptions; import io.rsocket.exceptions.MissingLeaseException; import io.rsocket.frame.FrameHeaderFlyweight; diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketReconnectTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java similarity index 98% rename from rsocket-core/src/test/java/io/rsocket/RSocketReconnectTest.java rename to rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java index b5ca4858e..f7ba12ada 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketReconnectTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java @@ -13,10 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import static org.junit.Assert.assertEquals; +import io.rsocket.RSocket; +import io.rsocket.RSocketFactory; import io.rsocket.test.util.TestClientTransport; import io.rsocket.transport.ClientTransport; import java.io.UncheckedIOException; diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java similarity index 98% rename from rsocket-core/src/test/java/io/rsocket/RSocketRequesterSubscribersTest.java rename to rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index b49dbe809..6b903292c 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -14,10 +14,11 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.rsocket.RSocket; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; import io.rsocket.frame.decoder.PayloadDecoder; diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketRequesterTerminationTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java similarity index 93% rename from rsocket-core/src/test/java/io/rsocket/RSocketRequesterTerminationTest.java rename to rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java index a2c17cf95..de6f86c57 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketRequesterTerminationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTerminationTest.java @@ -1,6 +1,8 @@ -package io.rsocket; +package io.rsocket.core; -import io.rsocket.RSocketRequesterTest.ClientSocketRule; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketRequesterTest.ClientSocketRule; import io.rsocket.util.EmptyPayload; import java.nio.channels.ClosedChannelException; import java.time.Duration; diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java similarity index 99% rename from rsocket-core/src/test/java/io/rsocket/RSocketRequesterTest.java rename to rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index a739f2e67..68d2f881b 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import static io.rsocket.frame.FrameHeaderFlyweight.frameType; import static io.rsocket.frame.FrameType.*; @@ -25,6 +25,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.*; diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java similarity index 98% rename from rsocket-core/src/test/java/io/rsocket/RSocketResponderTest.java rename to rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index b6281414d..10157532a 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import static io.rsocket.frame.FrameHeaderFlyweight.frameType; import static org.hamcrest.MatcherAssert.assertThat; @@ -23,6 +23,9 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.rsocket.AbstractRSocket; +import io.rsocket.Payload; +import io.rsocket.RSocket; import io.rsocket.frame.*; import io.rsocket.lease.ResponderLeaseHandler; import io.rsocket.test.util.TestDuplexConnection; diff --git a/rsocket-core/src/test/java/io/rsocket/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java similarity index 98% rename from rsocket-core/src/test/java/io/rsocket/RSocketTest.java rename to rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index 80865ec47..d5dcfa2f6 100644 --- a/rsocket-core/src/test/java/io/rsocket/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.is; @@ -23,6 +23,9 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.rsocket.AbstractRSocket; +import io.rsocket.Payload; +import io.rsocket.RSocket; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.CustomRSocketException; import io.rsocket.lease.RequesterLeaseHandler; diff --git a/rsocket-core/src/test/java/io/rsocket/ReconnectMonoTests.java b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java similarity index 99% rename from rsocket-core/src/test/java/io/rsocket/ReconnectMonoTests.java rename to rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java index fb7707509..968a1a793 100644 --- a/rsocket-core/src/test/java/io/rsocket/ReconnectMonoTests.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ReconnectMonoTests.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import static org.junit.Assert.assertEquals; diff --git a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java similarity index 99% rename from rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java rename to rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java index e6972eec0..daab5d246 100644 --- a/rsocket-core/src/test/java/io/rsocket/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -1,10 +1,11 @@ -package io.rsocket; +package io.rsocket.core; import static io.rsocket.transport.ServerTransport.ConnectionAcceptor; import static org.assertj.core.api.Assertions.assertThat; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.rsocket.*; import io.rsocket.exceptions.Exceptions; import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.ErrorFrameFlyweight; diff --git a/rsocket-core/src/test/java/io/rsocket/StreamIdSupplierTest.java b/rsocket-core/src/test/java/io/rsocket/core/StreamIdSupplierTest.java similarity index 99% rename from rsocket-core/src/test/java/io/rsocket/StreamIdSupplierTest.java rename to rsocket-core/src/test/java/io/rsocket/core/StreamIdSupplierTest.java index 766a6aaf7..00248b6d8 100644 --- a/rsocket-core/src/test/java/io/rsocket/StreamIdSupplierTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/StreamIdSupplierTest.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.rsocket; +package io.rsocket.core; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; diff --git a/rsocket-core/src/test/java/io/rsocket/TestingStuff.java b/rsocket-core/src/test/java/io/rsocket/core/TestingStuff.java similarity index 97% rename from rsocket-core/src/test/java/io/rsocket/TestingStuff.java rename to rsocket-core/src/test/java/io/rsocket/core/TestingStuff.java index 64c790053..e0ebf5064 100644 --- a/rsocket-core/src/test/java/io/rsocket/TestingStuff.java +++ b/rsocket-core/src/test/java/io/rsocket/core/TestingStuff.java @@ -1,4 +1,4 @@ -package io.rsocket; +package io.rsocket.core; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; @@ -16,6 +16,6 @@ public void testStuff() { ByteBuf byteBuf = Unpooled.wrappedBuffer(ByteBufUtil.decodeHexDump(f1)); System.out.println(ByteBufUtil.prettyHexDump(byteBuf)); - ConnectionSetupPayload.create(byteBuf); + new DefaultConnectionSetupPayload(byteBuf); } } From 8bdaee2846f0e2109e7e237287045fbff769c5cf Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 9 Apr 2020 20:04:14 +0300 Subject: [PATCH 131/181] [Bugfix] Incorrect Behaviour of RequestChannel (#736) * provides failing test Signed-off-by: Oleh Dokuka * provides requestChannel bugfix Signed-off-by: Oleh Dokuka * more fixes Signed-off-by: Oleh Dokuka * rollbacks unwanted changes Signed-off-by: Oleh Dokuka * fixes incorrect requestN behaviour on the requester side Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/RSocketRequester.java | 184 +++++++------ .../io/rsocket/core/RSocketResponder.java | 146 +++++------ .../main/java/io/rsocket/frame/FrameUtil.java | 10 + .../RateLimitableRequestPublisher.java | 242 ------------------ .../core/RSocketRequesterSubscribersTest.java | 25 +- .../io/rsocket/core/RSocketRequesterTest.java | 47 ++++ .../java/io/rsocket/core/RSocketTest.java | 18 ++ .../RateLimitableRequestPublisherTest.java | 140 ---------- .../java/io/rsocket/test/TestRSocket.java | 2 +- .../java/io/rsocket/test/TransportTest.java | 11 +- 10 files changed, 261 insertions(+), 564 deletions(-) delete mode 100755 rsocket-core/src/main/java/io/rsocket/internal/RateLimitableRequestPublisher.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/internal/RateLimitableRequestPublisherTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index e39fa7a29..6c26361a2 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -40,7 +40,6 @@ import io.rsocket.frame.RequestResponseFrameFlyweight; import io.rsocket.frame.RequestStreamFrameFlyweight; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.RateLimitableRequestPublisher; import io.rsocket.internal.SynchronizedIntObjectHashMap; import io.rsocket.internal.UnboundedProcessor; import io.rsocket.internal.UnicastMonoEmpty; @@ -51,6 +50,7 @@ import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.util.MonoLifecycleHandler; import java.nio.channels.ClosedChannelException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; import java.util.function.LongConsumer; @@ -60,6 +60,7 @@ import org.reactivestreams.Processor; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -84,7 +85,7 @@ class RSocketRequester implements RSocket { private final PayloadDecoder payloadDecoder; private final Consumer errorConsumer; private final StreamIdSupplier streamIdSupplier; - private final IntObjectMap senders; + private final IntObjectMap senders; private final IntObjectMap> receivers; private final UnboundedProcessor sendProcessor; private final RequesterLeaseHandler leaseHandler; @@ -258,6 +259,7 @@ private Flux handleRequestStream(final Payload payload) { final UnboundedProcessor sendProcessor = this.sendProcessor; final UnicastProcessor receiver = UnicastProcessor.create(); + final AtomicBoolean payloadReleasedFlag = new AtomicBoolean(false); receivers.put(streamId, receiver); @@ -279,7 +281,9 @@ public void accept(long n) { n, payload.sliceMetadata().retain(), payload.sliceData().retain())); - payload.release(); + if (!payloadReleasedFlag.getAndSet(true)) { + payload.release(); + } } else if (contains(streamId) && !receiver.isDisposed()) { sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); } @@ -293,6 +297,9 @@ public void accept(long n) { }) .doOnCancel( () -> { + if (!payloadReleasedFlag.getAndSet(true)) { + payload.release(); + } if (contains(streamId) && !receiver.isDisposed()) { sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); } @@ -306,10 +313,67 @@ private Flux handleChannel(Flux request) { return Flux.error(err); } + return request.switchOnFirst( + (s, flux) -> { + Payload payload = s.get(); + if (payload != null) { + return handleChannel(payload, flux); + } else { + return flux; + } + }, + false); + } + + private Flux handleChannel(Payload initialPayload, Flux inboundFlux) { final UnboundedProcessor sendProcessor = this.sendProcessor; - final UnicastProcessor receiver = UnicastProcessor.create(); + final AtomicBoolean payloadReleasedFlag = new AtomicBoolean(false); final int streamId = streamIdSupplier.nextStreamId(receivers); + final UnicastProcessor receiver = UnicastProcessor.create(); + final BaseSubscriber upstreamSubscriber = + new BaseSubscriber() { + + boolean first = true; + + @Override + protected void hookOnSubscribe(Subscription subscription) { + // noops + } + + @Override + protected void hookOnNext(Payload payload) { + if (first) { + // need to skip first since we have already sent it + first = false; + return; + } + final ByteBuf frame = + PayloadFrameFlyweight.encode(allocator, streamId, false, false, true, payload); + + sendProcessor.onNext(frame); + payload.release(); + } + + @Override + protected void hookOnComplete() { + ByteBuf frame = PayloadFrameFlyweight.encodeComplete(allocator, streamId); + sendProcessor.onNext(frame); + } + + @Override + protected void hookOnError(Throwable t) { + ByteBuf frame = ErrorFrameFlyweight.encode(allocator, streamId, t); + sendProcessor.onNext(frame); + receiver.onError(t); + } + + @Override + protected void hookFinally(SignalType type) { + senders.remove(streamId, this); + } + }; + return receiver .doOnRequest( new LongConsumer() { @@ -320,85 +384,47 @@ private Flux handleChannel(Flux request) { public void accept(long n) { if (firstRequest) { firstRequest = false; - request - .transform( - f -> { - RateLimitableRequestPublisher wrapped = - RateLimitableRequestPublisher.wrap(f, Queues.SMALL_BUFFER_SIZE); - // Need to set this to one for first the frame - wrapped.request(1); - senders.put(streamId, wrapped); - receivers.put(streamId, receiver); - - return wrapped; - }) - .subscribe( - new BaseSubscriber() { - - boolean firstPayload = true; - - @Override - protected void hookOnNext(Payload payload) { - final ByteBuf frame; - - if (firstPayload) { - firstPayload = false; - frame = - RequestChannelFrameFlyweight.encode( - allocator, - streamId, - false, - false, - n, - payload.sliceMetadata().retain(), - payload.sliceData().retain()); - } else { - frame = - PayloadFrameFlyweight.encode( - allocator, streamId, false, false, true, payload); - } - - sendProcessor.onNext(frame); - payload.release(); - } - - @Override - protected void hookOnComplete() { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext( - PayloadFrameFlyweight.encodeComplete(allocator, streamId)); - } - if (firstPayload) { - receiver.onComplete(); - } - } - - @Override - protected void hookOnError(Throwable t) { - errorConsumer.accept(t); - receiver.dispose(); - } - }); - } else { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); + senders.put(streamId, upstreamSubscriber); + receivers.put(streamId, receiver); + + inboundFlux.limitRate(Queues.SMALL_BUFFER_SIZE).subscribe(upstreamSubscriber); + if (!payloadReleasedFlag.getAndSet(true)) { + ByteBuf frame = + RequestChannelFrameFlyweight.encode( + allocator, + streamId, + false, + false, + n, + initialPayload.sliceMetadata().retain(), + initialPayload.sliceData().retain()); + + sendProcessor.onNext(frame); + + initialPayload.release(); } + } else { + sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); } } }) .doOnError( t -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t)); + if (receivers.remove(streamId, receiver)) { + upstreamSubscriber.cancel(); } }) + .doOnComplete(() -> receivers.remove(streamId, receiver)) .doOnCancel( () -> { - if (contains(streamId) && !receiver.isDisposed()) { + if (!payloadReleasedFlag.getAndSet(true)) { + initialPayload.release(); + } + if (receivers.remove(streamId, receiver)) { sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + upstreamSubscriber.cancel(); } - }) - .doFinally(s -> removeStreamReceiverAndSender(streamId)); + }); } private Mono handleMetadataPush(Payload payload) { @@ -487,7 +513,7 @@ private void handleFrame(int streamId, FrameType type, ByteBuf frame) { break; case CANCEL: { - RateLimitableRequestPublisher sender = senders.remove(streamId); + Subscription sender = senders.remove(streamId); if (sender != null) { sender.cancel(); } @@ -498,7 +524,7 @@ private void handleFrame(int streamId, FrameType type, ByteBuf frame) { break; case REQUEST_N: { - RateLimitableRequestPublisher sender = senders.get(streamId); + Subscription sender = senders.get(streamId); if (sender != null) { int n = RequestNFrameFlyweight.requestN(frame); sender.request(n >= Integer.MAX_VALUE ? Long.MAX_VALUE : n); @@ -606,18 +632,6 @@ private void removeStreamReceiver(int streamId) { } } - private void removeStreamReceiverAndSender(int streamId) { - /*on termination senders & receivers are explicitly cleared to avoid removing from map while iterating over one - of its views*/ - if (terminationError == null) { - receivers.remove(streamId); - RateLimitableRequestPublisher sender = senders.remove(streamId); - if (sender != null) { - sender.cancel(); - } - } - } - private void handleSendProcessorError(Throwable t) { connection.dispose(); } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index d742300d1..de6e8ad23 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -27,11 +27,11 @@ import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.frame.*; import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.RateLimitableRequestPublisher; import io.rsocket.internal.SynchronizedIntObjectHashMap; import io.rsocket.internal.UnboundedProcessor; import io.rsocket.lease.ResponderLeaseHandler; import java.util.function.Consumer; +import java.util.function.LongConsumer; import org.reactivestreams.Processor; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -51,7 +51,6 @@ class RSocketResponder implements ResponderRSocket { private final Consumer errorConsumer; private final ResponderLeaseHandler leaseHandler; - private final IntObjectMap sendingLimitableSubscriptions; private final IntObjectMap sendingSubscriptions; private final IntObjectMap> channelProcessors; @@ -75,7 +74,6 @@ class RSocketResponder implements ResponderRSocket { this.payloadDecoder = payloadDecoder; this.errorConsumer = errorConsumer; this.leaseHandler = leaseHandler; - this.sendingLimitableSubscriptions = new SynchronizedIntObjectHashMap<>(); this.sendingSubscriptions = new SynchronizedIntObjectHashMap<>(); this.channelProcessors = new SynchronizedIntObjectHashMap<>(); @@ -114,17 +112,6 @@ private void handleSendProcessorError(Throwable t) { } }); - sendingLimitableSubscriptions - .values() - .forEach( - subscription -> { - try { - subscription.cancel(); - } catch (Throwable e) { - errorConsumer.accept(e); - } - }); - channelProcessors .values() .forEach( @@ -153,17 +140,6 @@ private void handleSendProcessorCancel(SignalType t) { } }); - sendingLimitableSubscriptions - .values() - .forEach( - subscription -> { - try { - subscription.cancel(); - } catch (Throwable e) { - errorConsumer.accept(e); - } - }); - channelProcessors .values() .forEach( @@ -280,9 +256,6 @@ private void cleanup() { private synchronized void cleanUpSendingSubscriptions() { sendingSubscriptions.values().forEach(Subscription::cancel); sendingSubscriptions.clear(); - - sendingLimitableSubscriptions.values().forEach(Subscription::cancel); - sendingLimitableSubscriptions.clear(); } private synchronized void cleanUpChannelProcessors() { @@ -388,16 +361,10 @@ protected void hookFinally(SignalType type) { } private void handleRequestResponse(int streamId, Mono response) { - response.subscribe( + final BaseSubscriber subscriber = new BaseSubscriber() { private boolean isEmpty = true; - @Override - protected void hookOnSubscribe(Subscription subscription) { - sendingSubscriptions.put(streamId, subscription); - subscription.request(Long.MAX_VALUE); - } - @Override protected void hookOnNext(Payload payload) { if (isEmpty) { @@ -431,55 +398,56 @@ protected void hookOnComplete() { @Override protected void hookFinally(SignalType type) { - sendingSubscriptions.remove(streamId); + sendingSubscriptions.remove(streamId, this); } - }); + }; + + sendingSubscriptions.put(streamId, subscriber); + response.subscribe(subscriber); } private void handleStream(int streamId, Flux response, int initialRequestN) { - response - .transform( - frameFlux -> { - RateLimitableRequestPublisher payloads = - RateLimitableRequestPublisher.wrap(frameFlux, Queues.SMALL_BUFFER_SIZE); - sendingLimitableSubscriptions.put(streamId, payloads); - payloads.request( - initialRequestN >= Integer.MAX_VALUE ? Long.MAX_VALUE : initialRequestN); - return payloads; - }) - .subscribe( - new BaseSubscriber() { - - @Override - protected void hookOnNext(Payload payload) { - ByteBuf byteBuf; - try { - byteBuf = PayloadFrameFlyweight.encodeNext(allocator, streamId, payload); - } catch (Throwable t) { - payload.release(); - throw Exceptions.propagate(t); - } - - payload.release(); - - sendProcessor.onNext(byteBuf); - } + final BaseSubscriber subscriber = + new BaseSubscriber() { - @Override - protected void hookOnComplete() { - sendProcessor.onNext(PayloadFrameFlyweight.encodeComplete(allocator, streamId)); - } + @Override + protected void hookOnSubscribe(Subscription s) { + s.request(initialRequestN >= Integer.MAX_VALUE ? Long.MAX_VALUE : initialRequestN); + } - @Override - protected void hookOnError(Throwable throwable) { - handleError(streamId, throwable); - } + @Override + protected void hookOnNext(Payload payload) { + ByteBuf byteBuf; + try { + byteBuf = PayloadFrameFlyweight.encodeNext(allocator, streamId, payload); + } catch (Throwable t) { + payload.release(); + throw Exceptions.propagate(t); + } - @Override - protected void hookFinally(SignalType type) { - sendingLimitableSubscriptions.remove(streamId); - } - }); + payload.release(); + + sendProcessor.onNext(byteBuf); + } + + @Override + protected void hookOnComplete() { + sendProcessor.onNext(PayloadFrameFlyweight.encodeComplete(allocator, streamId)); + } + + @Override + protected void hookOnError(Throwable throwable) { + handleError(streamId, throwable); + } + + @Override + protected void hookFinally(SignalType type) { + sendingSubscriptions.remove(streamId); + } + }; + + sendingSubscriptions.put(streamId, subscriber); + response.limitRate(Queues.SMALL_BUFFER_SIZE).subscribe(subscriber); } private void handleChannel(int streamId, Payload payload, int initialRequestN) { @@ -492,7 +460,21 @@ private void handleChannel(int streamId, Payload payload, int initialRequestN) { () -> sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId))) .doOnError(t -> handleError(streamId, t)) .doOnRequest( - l -> sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, l))) + new LongConsumer() { + boolean first = true; + + @Override + public void accept(long l) { + long n; + if (first) { + first = false; + n = l - 1L; + } else { + n = l; + } + sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); + } + }) .doFinally(signalType -> channelProcessors.remove(streamId)); // not chained, as the payload should be enqueued in the Unicast processor before this method @@ -525,10 +507,6 @@ protected void hookOnError(Throwable throwable) { private void handleCancelFrame(int streamId) { Subscription subscription = sendingSubscriptions.remove(streamId); - if (subscription == null) { - subscription = sendingLimitableSubscriptions.remove(streamId); - } - if (subscription != null) { subscription.cancel(); } @@ -542,10 +520,6 @@ private void handleError(int streamId, Throwable t) { private void handleRequestN(int streamId, ByteBuf frame) { Subscription subscription = sendingSubscriptions.get(streamId); - if (subscription == null) { - subscription = sendingLimitableSubscriptions.get(streamId); - } - if (subscription != null) { int n = RequestNFrameFlyweight.requestN(frame); subscription.request(n >= Integer.MAX_VALUE ? Long.MAX_VALUE : n); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java index 0d2175fb6..6662d34af 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java @@ -22,6 +22,16 @@ public static String toString(ByteBuf frame) { .append(Integer.toBinaryString(FrameHeaderFlyweight.flags(frame))) .append(" Length: " + frame.readableBytes()); + if (frameType.hasInitialRequestN()) { + payload + .append(" InitialRequestN: ") + .append(RequestStreamFrameFlyweight.initialRequestN(frame)); + } + + if (frameType == FrameType.REQUEST_N) { + payload.append(" RequestN: ").append(RequestNFrameFlyweight.requestN(frame)); + } + if (FrameHeaderFlyweight.hasMetadata(frame)) { payload.append("\nMetadata:\n"); diff --git a/rsocket-core/src/main/java/io/rsocket/internal/RateLimitableRequestPublisher.java b/rsocket-core/src/main/java/io/rsocket/internal/RateLimitableRequestPublisher.java deleted file mode 100755 index cdb0d0c0c..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/RateLimitableRequestPublisher.java +++ /dev/null @@ -1,242 +0,0 @@ -/* - * Copyright 2015-2018 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.internal; - -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import javax.annotation.Nullable; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.CoreSubscriber; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Operators; - -/** */ -public class RateLimitableRequestPublisher extends Flux implements Subscription { - - private static final int NOT_CANCELED_STATE = 0; - private static final int CANCELED_STATE = 1; - - private final Publisher source; - - private volatile int canceled; - private static final AtomicIntegerFieldUpdater CANCELED = - AtomicIntegerFieldUpdater.newUpdater(RateLimitableRequestPublisher.class, "canceled"); - - private final long prefetch; - private final long limit; - - private long externalRequested; // need sync - private int pendingToFulfil; // need sync since should be checked/zerroed in onNext - // and increased in request - private int deliveredElements; // no need to sync since increased zerroed only in - // the request method - - private boolean subscribed; - - private @Nullable Subscription internalSubscription; - - private RateLimitableRequestPublisher(Publisher source, long prefetch) { - this.source = source; - this.prefetch = prefetch; - this.limit = prefetch == Integer.MAX_VALUE ? Integer.MAX_VALUE : (prefetch - (prefetch >> 2)); - } - - public static RateLimitableRequestPublisher wrap(Publisher source, long prefetch) { - return new RateLimitableRequestPublisher<>(source, prefetch); - } - - @Override - public void subscribe(CoreSubscriber destination) { - synchronized (this) { - if (subscribed) { - throw new IllegalStateException("only one subscriber at a time"); - } - - subscribed = true; - } - final InnerOperator s = new InnerOperator(destination); - - source.subscribe(s); - destination.onSubscribe(s); - } - - @Override - public void request(long n) { - synchronized (this) { - long requested = externalRequested; - if (requested == Long.MAX_VALUE) { - return; - } - externalRequested = Operators.addCap(n, requested); - } - - requestN(); - } - - private void requestN() { - final long r; - final Subscription s; - - synchronized (this) { - s = internalSubscription; - if (s == null) { - return; - } - - final long er = externalRequested; - final long p = prefetch; - final int pendingFulfil = pendingToFulfil; - - if (er != Long.MAX_VALUE || p != Integer.MAX_VALUE) { - // shortcut - if (pendingFulfil == p) { - return; - } - - r = Math.min(p - pendingFulfil, er); - if (er != Long.MAX_VALUE) { - externalRequested -= r; - } - if (p != Integer.MAX_VALUE) { - pendingToFulfil += r; - } - } else { - r = Long.MAX_VALUE; - } - } - - if (r > 0) { - s.request(r); - } - } - - public void cancel() { - if (!isCanceled() && CANCELED.compareAndSet(this, NOT_CANCELED_STATE, CANCELED_STATE)) { - Subscription s; - - synchronized (this) { - s = internalSubscription; - internalSubscription = null; - subscribed = false; - } - - if (s != null) { - s.cancel(); - } - } - } - - private boolean isCanceled() { - return canceled == CANCELED_STATE; - } - - private class InnerOperator implements CoreSubscriber, Subscription { - final Subscriber destination; - - private InnerOperator(Subscriber destination) { - this.destination = destination; - } - - @Override - public void onSubscribe(Subscription s) { - synchronized (RateLimitableRequestPublisher.this) { - RateLimitableRequestPublisher.this.internalSubscription = s; - - if (isCanceled()) { - s.cancel(); - subscribed = false; - RateLimitableRequestPublisher.this.internalSubscription = null; - } - } - - requestN(); - } - - @Override - public void onNext(T t) { - try { - destination.onNext(t); - - if (prefetch == Integer.MAX_VALUE) { - return; - } - - final long l = limit; - int d = deliveredElements + 1; - - if (d == l) { - d = 0; - final long r; - final Subscription s; - - synchronized (RateLimitableRequestPublisher.this) { - long er = externalRequested; - s = internalSubscription; - - if (s == null) { - return; - } - - if (er >= l) { - er -= l; - // keep pendingToFulfil as is since it is eq to prefetch - r = l; - } else { - pendingToFulfil -= l; - if (er > 0) { - r = er; - er = 0; - pendingToFulfil += r; - } else { - r = 0; - } - } - - externalRequested = er; - } - - if (r > 0) { - s.request(r); - } - } - - deliveredElements = d; - } catch (Throwable e) { - onError(e); - } - } - - @Override - public void onError(Throwable t) { - destination.onError(t); - } - - @Override - public void onComplete() { - destination.onComplete(); - } - - @Override - public void request(long n) {} - - @Override - public void cancel() { - RateLimitableRequestPublisher.this.cancel(); - } - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index 6b903292c..8a2e114cc 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -39,7 +39,6 @@ import org.junit.jupiter.params.provider.MethodSource; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import reactor.test.StepVerifier; class RSocketRequesterSubscribersTest { @@ -76,9 +75,15 @@ void setUp() { @MethodSource("allInteractions") void multiSubscriber(Function> interaction) { RSocket multiSubsRSocket = new MultiSubscriberRSocket(rSocketRequester); - Flux response = Flux.from(interaction.apply(multiSubsRSocket)).take(Duration.ofMillis(10)); - StepVerifier.create(response).expectComplete().verify(Duration.ofSeconds(5)); - StepVerifier.create(response).expectComplete().verify(Duration.ofSeconds(5)); + Flux response = Flux.from(interaction.apply(multiSubsRSocket)); + StepVerifier.withVirtualTime(() -> response.take(Duration.ofMillis(10))) + .thenAwait(Duration.ofMillis(10)) + .expectComplete() + .verify(Duration.ofSeconds(5)); + StepVerifier.withVirtualTime(() -> response.take(Duration.ofMillis(10))) + .thenAwait(Duration.ofMillis(10)) + .expectComplete() + .verify(Duration.ofSeconds(5)); Assertions.assertThat(requestFramesCount(connection.getSent())).isEqualTo(2); } @@ -86,9 +91,13 @@ void multiSubscriber(Function> interaction) { @ParameterizedTest @MethodSource("allInteractions") void singleSubscriber(Function> interaction) { - Flux response = Flux.from(interaction.apply(rSocketRequester)).take(Duration.ofMillis(10)); - StepVerifier.create(response).expectComplete().verify(Duration.ofSeconds(5)); - StepVerifier.create(response) + Flux response = Flux.from(interaction.apply(rSocketRequester)); + StepVerifier.withVirtualTime(() -> response.take(Duration.ofMillis(10))) + .thenAwait(Duration.ofMillis(10)) + .expectComplete() + .verify(Duration.ofSeconds(5)); + StepVerifier.withVirtualTime(() -> response.take(Duration.ofMillis(10))) + .thenAwait(Duration.ofMillis(10)) .expectError(IllegalStateException.class) .verify(Duration.ofSeconds(5)); @@ -115,7 +124,7 @@ static Stream>> allInteractions() { rSocket -> rSocket.fireAndForget(DefaultPayload.create("test")), rSocket -> rSocket.requestResponse(DefaultPayload.create("test")), rSocket -> rSocket.requestStream(DefaultPayload.create("test")), - rSocket -> rSocket.requestChannel(Mono.just(DefaultPayload.create("test"))), + // rSocket -> rSocket.requestChannel(Mono.just(DefaultPayload.create("test"))), rSocket -> rSocket.metadataPush(DefaultPayload.create("test"))); } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index 68d2f881b..20b1825fa 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -25,6 +25,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.CharsetUtil; import io.rsocket.Payload; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.RejectedSetupException; @@ -36,6 +37,7 @@ import io.rsocket.util.MultiSubscriberRSocket; import java.time.Duration; import java.util.ArrayList; +import java.util.Iterator; import java.util.List; import java.util.stream.Collectors; import org.assertj.core.api.Assertions; @@ -195,6 +197,19 @@ public void testChannelRequestCancellation() { .blockFirst(); } + @Test + public void testChannelRequestCancellation2() { + MonoProcessor cancelled = MonoProcessor.create(); + Flux request = + Flux.just(EmptyPayload.INSTANCE).repeat(259).doOnCancel(cancelled::onComplete); + rule.socket.requestChannel(request).subscribe().dispose(); + Flux.first( + cancelled, + Flux.error(new IllegalStateException("Channel request not cancelled")) + .delaySubscription(Duration.ofSeconds(1))) + .blockFirst(); + } + @Test public void testChannelRequestServerSideCancellation() { MonoProcessor cancelled = MonoProcessor.create(); @@ -215,6 +230,38 @@ public void testChannelRequestServerSideCancellation() { Assertions.assertThat(request.isDisposed()).isTrue(); } + @Test + public void testCorrectFrameOrder() { + MonoProcessor delayer = MonoProcessor.create(); + BaseSubscriber subscriber = + new BaseSubscriber() { + @Override + protected void hookOnSubscribe(Subscription subscription) {} + }; + rule.socket + .requestChannel( + Flux.concat(Flux.just(0).delayUntil(i -> delayer), Flux.range(1, 999)) + .map(i -> DefaultPayload.create(i + ""))) + .subscribe(subscriber); + + subscriber.request(1); + subscriber.request(Long.MAX_VALUE); + delayer.onComplete(); + + Iterator iterator = rule.connection.getSent().iterator(); + + ByteBuf initialFrame = iterator.next(); + + Assertions.assertThat(FrameHeaderFlyweight.frameType(initialFrame)).isEqualTo(REQUEST_CHANNEL); + Assertions.assertThat(RequestChannelFrameFlyweight.initialRequestN(initialFrame)) + .isEqualTo(Integer.MAX_VALUE); + Assertions.assertThat( + RequestChannelFrameFlyweight.data(initialFrame).toString(CharsetUtil.UTF_8)) + .isEqualTo("0"); + + Assertions.assertThat(iterator.hasNext()).isFalse(); + } + public int sendRequestResponse(Publisher response) { Subscriber sub = TestSubscriber.create(); response.subscribe(sub); diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index d5dcfa2f6..b18fad890 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -35,6 +35,8 @@ import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; import java.util.ArrayList; +import java.util.concurrent.atomic.AtomicReference; +import org.assertj.core.api.Assertions; import org.hamcrest.MatcherAssert; import org.junit.Assert; import org.junit.Rule; @@ -133,6 +135,22 @@ public void testChannel() throws Exception { StepVerifier.create(responses).expectNextCount(10).expectComplete().verify(); } + @Test(timeout = 2000) + public void testErrorPropagatesCorrectly() { + AtomicReference error = new AtomicReference<>(); + rule.setRequestAcceptor( + new AbstractRSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads).doOnError(error::set); + } + }); + Flux requests = Flux.error(new RuntimeException("test")); + Flux responses = rule.crs.requestChannel(requests); + StepVerifier.create(responses).expectErrorMessage("test").verify(); + Assertions.assertThat(error.get()).isNull(); + } + public static class SocketRule extends ExternalResource { DirectProcessor serverProcessor; diff --git a/rsocket-core/src/test/java/io/rsocket/internal/RateLimitableRequestPublisherTest.java b/rsocket-core/src/test/java/io/rsocket/internal/RateLimitableRequestPublisherTest.java deleted file mode 100644 index af4c528e9..000000000 --- a/rsocket-core/src/test/java/io/rsocket/internal/RateLimitableRequestPublisherTest.java +++ /dev/null @@ -1,140 +0,0 @@ -package io.rsocket.internal; - -import static org.junit.jupiter.api.Assertions.*; - -import java.time.Duration; -import java.util.concurrent.ThreadLocalRandom; -import java.util.function.Consumer; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; -import reactor.core.scheduler.Schedulers; -import reactor.test.StepVerifier; - -class RateLimitableRequestPublisherTest { - - @Test - public void testThatRequest1WillBePropagatedUpstream() { - Flux source = - Flux.just(1) - .subscribeOn(Schedulers.parallel()) - .doOnRequest(r -> Assertions.assertThat(r).isLessThanOrEqualTo(128)); - - RateLimitableRequestPublisher rateLimitableRequestPublisher = - RateLimitableRequestPublisher.wrap(source, 128); - - StepVerifier.create(rateLimitableRequestPublisher) - .then(() -> rateLimitableRequestPublisher.request(1)) - .expectNext(1) - .expectComplete() - .verify(Duration.ofMillis(1000)); - } - - @Test - public void testThatRequest256WillBePropagatedToUpstreamWithLimitedRate() { - Flux source = - Flux.range(0, 256) - .subscribeOn(Schedulers.parallel()) - .doOnRequest(r -> Assertions.assertThat(r).isLessThanOrEqualTo(128)); - - RateLimitableRequestPublisher rateLimitableRequestPublisher = - RateLimitableRequestPublisher.wrap(source, 128); - - StepVerifier.create(rateLimitableRequestPublisher) - .then(() -> rateLimitableRequestPublisher.request(256)) - .expectNextCount(256) - .expectComplete() - .verify(Duration.ofMillis(1000)); - } - - @Test - public void testThatRequest256WillBePropagatedToUpstreamWithLimitedRateInFewSteps() { - Flux source = - Flux.range(0, 256) - .subscribeOn(Schedulers.parallel()) - .doOnRequest(r -> Assertions.assertThat(r).isLessThanOrEqualTo(128)); - - RateLimitableRequestPublisher rateLimitableRequestPublisher = - RateLimitableRequestPublisher.wrap(source, 128); - - StepVerifier.create(rateLimitableRequestPublisher) - .then(() -> rateLimitableRequestPublisher.request(10)) - .expectNextCount(5) - .then(() -> rateLimitableRequestPublisher.request(128)) - .expectNextCount(133) - .expectNoEvent(Duration.ofMillis(10)) - .then(() -> rateLimitableRequestPublisher.request(Long.MAX_VALUE)) - .expectNextCount(118) - .expectComplete() - .verify(Duration.ofMillis(1000)); - } - - @Test - public void testThatRequestInRandomFashionWillBePropagatedToUpstreamWithLimitedRateInFewSteps() { - Flux source = - Flux.range(0, 10000000) - .subscribeOn(Schedulers.parallel()) - .doOnRequest(r -> Assertions.assertThat(r).isLessThanOrEqualTo(128)); - - RateLimitableRequestPublisher rateLimitableRequestPublisher = - RateLimitableRequestPublisher.wrap(source, 128); - - StepVerifier.create(rateLimitableRequestPublisher) - .then( - () -> - Flux.interval(Duration.ofMillis(1000)) - .onBackpressureDrop() - .subscribe( - new Consumer() { - int count = 10000000; - - @Override - public void accept(Long __) { - int random = ThreadLocalRandom.current().nextInt(1, 512); - - long request = Math.min(random, count); - - count -= request; - - rateLimitableRequestPublisher.request(count); - } - })) - .expectNextCount(10000000) - .expectComplete() - .verify(Duration.ofMillis(30000)); - } - - @Test - public void testThatRequestLongMaxValueWillBeDeliveredInSeparateChunks() { - Flux source = - Flux.range(0, 10000000) - .subscribeOn(Schedulers.parallel()) - .doOnRequest(r -> Assertions.assertThat(r).isLessThanOrEqualTo(128)); - - RateLimitableRequestPublisher rateLimitableRequestPublisher = - RateLimitableRequestPublisher.wrap(source, 128); - - StepVerifier.create(rateLimitableRequestPublisher) - .then(() -> rateLimitableRequestPublisher.request(Long.MAX_VALUE)) - .expectNextCount(10000000) - .expectComplete() - .verify(Duration.ofMillis(30000)); - } - - @Test - public void testThatRequestLongMaxWithIntegerMaxValuePrefetchWillBeDeliveredAsLongMaxValue() { - Flux source = - Flux.range(0, 10000000) - .subscribeOn(Schedulers.parallel()) - .doOnRequest(r -> Assertions.assertThat(r).isEqualTo(Long.MAX_VALUE)); - - RateLimitableRequestPublisher rateLimitableRequestPublisher = - RateLimitableRequestPublisher.wrap(source, Integer.MAX_VALUE); - - StepVerifier.create(rateLimitableRequestPublisher) - .then(() -> rateLimitableRequestPublisher.request(Long.MAX_VALUE)) - .expectNextCount(10000000) - .expectComplete() - .verify(Duration.ofMillis(30000)); - } -} diff --git a/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java b/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java index 57a2e5c3c..26163d3a6 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java @@ -55,6 +55,6 @@ public Mono fireAndForget(Payload payload) { @Override public Flux requestChannel(Publisher payloads) { // TODO is defensive copy neccesary? - return Flux.from(payloads).map(DefaultPayload::create); + return Flux.from(payloads).map(Payload::retain); } } diff --git a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java index 6721acf2c..1c00b0502 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -26,11 +26,13 @@ import java.io.BufferedReader; import java.io.InputStreamReader; import java.time.Duration; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.zip.GZIPInputStream; +import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -215,14 +217,19 @@ default void requestChannel2_000_000() { @DisplayName("makes 1 requestChannel request with 3 payloads") @Test default void requestChannel3() { - Flux payloads = Flux.range(0, 3).map(this::createTestPayload); + AtomicLong requested = new AtomicLong(); + Flux payloads = + Flux.range(0, 3).doOnRequest(requested::addAndGet).map(this::createTestPayload); getClient() .requestChannel(payloads) - .as(StepVerifier::create) + .as(publisher -> StepVerifier.create(publisher, 3)) .expectNextCount(3) .expectComplete() .verify(getTimeout()); + + Assertions.assertThat(requested.get()) + .isEqualTo(256L); // 256 because of eager behavior of limitRate } @DisplayName("makes 1 requestChannel request with 512 payloads") From c2475c2acd0399b2696c917f9bf1abb1974b8988 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 9 Apr 2020 23:13:49 +0300 Subject: [PATCH 132/181] Payload length validation (#768) * provides failing test Signed-off-by: Oleh Dokuka * provides requestChannel bugfix Signed-off-by: Oleh Dokuka * more fixes Signed-off-by: Oleh Dokuka * initial Signed-off-by: Oleh Dokuka * provides payload length validation in case fragmentation is disabled Signed-off-by: Oleh Dokuka * provides payload validation Signed-off-by: Oleh Dokuka * fixes tests and changes error message Signed-off-by: Oleh Dokuka * rollbacks unwanted changes Signed-off-by: Oleh Dokuka * moves validator to core package and makes it pkg private Signed-off-by: Oleh Dokuka * simplifies validation utils. puts error message to constant field Signed-off-by: Oleh Dokuka * fixes IDE warning Signed-off-by: Oleh Dokuka --- .../core/DefaultClientRSocketFactory.java | 4 +- .../core/DefaultServerRSocketFactory.java | 4 +- .../rsocket/core/PayloadValidationUtils.java | 32 ++++++ .../io/rsocket/core/RSocketRequester.java | 43 ++++++++ .../io/rsocket/core/RSocketResponder.java | 26 ++++- .../fragmentation/FrameFragmenter.java | 9 +- .../java/io/rsocket/core/KeepAliveTest.java | 2 + .../core/PayloadValidationUtilsTest.java | 99 +++++++++++++++++++ .../io/rsocket/core/RSocketLeaseTest.java | 4 +- .../core/RSocketRequesterSubscribersTest.java | 1 + .../io/rsocket/core/RSocketRequesterTest.java | 87 +++++++++++++++- .../io/rsocket/core/RSocketResponderTest.java | 60 ++++++++++- .../java/io/rsocket/core/RSocketTest.java | 4 +- .../io/rsocket/core/SetupRejectionTest.java | 2 + 14 files changed, 367 insertions(+), 10 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/core/PayloadValidationUtils.java create mode 100644 rsocket-core/src/test/java/io/rsocket/core/PayloadValidationUtilsTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java index ce43cd1fd..b7cad7042 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java @@ -331,6 +331,7 @@ public Mono start() { payloadDecoder, errorConsumer, StreamIdSupplier.clientSupplier(), + mtu, keepAliveTickPeriod(), keepAliveTimeout(), keepAliveHandler, @@ -379,7 +380,8 @@ public Mono start() { wrappedRSocketHandler, payloadDecoder, errorConsumer, - responderLeaseHandler); + responderLeaseHandler, + mtu); return wrappedConnection .sendOne(setupFrame) diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java index f2acb9af0..85543181a 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java @@ -281,6 +281,7 @@ private Mono acceptSetup( payloadDecoder, errorConsumer, StreamIdSupplier.serverSupplier(), + mtu, setupPayload.keepAliveInterval(), setupPayload.keepAliveMaxLifetime(), keepAliveHandler, @@ -317,7 +318,8 @@ private Mono acceptSetup( wrappedRSocketHandler, payloadDecoder, errorConsumer, - responderLeaseHandler); + responderLeaseHandler, + mtu); }) .doFinally(signalType -> setupPayload.release()) .then(); diff --git a/rsocket-core/src/main/java/io/rsocket/core/PayloadValidationUtils.java b/rsocket-core/src/main/java/io/rsocket/core/PayloadValidationUtils.java new file mode 100644 index 000000000..3b6b375d1 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/PayloadValidationUtils.java @@ -0,0 +1,32 @@ +package io.rsocket.core; + +import io.rsocket.Payload; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameLengthFlyweight; + +final class PayloadValidationUtils { + static final String INVALID_PAYLOAD_ERROR_MESSAGE = + "The payload is too big to send as a single frame with a 24-bit encoded length. Consider enabling fragmentation via RSocketFactory."; + + static boolean isValid(int mtu, Payload payload) { + if (mtu > 0) { + return true; + } + + if (payload.hasMetadata()) { + return (((FrameHeaderFlyweight.size() + + FrameLengthFlyweight.FRAME_LENGTH_SIZE + + FrameHeaderFlyweight.size() + + payload.data().readableBytes() + + payload.metadata().readableBytes()) + & ~FrameLengthFlyweight.FRAME_LENGTH_MASK) + == 0); + } else { + return (((FrameHeaderFlyweight.size() + + payload.data().readableBytes() + + FrameLengthFlyweight.FRAME_LENGTH_SIZE) + & ~FrameLengthFlyweight.FRAME_LENGTH_MASK) + == 0); + } + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 6c26361a2..fc3175b15 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -16,6 +16,7 @@ package io.rsocket.core; +import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; import static io.rsocket.keepalive.KeepAliveSupport.ClientKeepAliveSupport; import static io.rsocket.keepalive.KeepAliveSupport.KeepAlive; @@ -88,6 +89,7 @@ class RSocketRequester implements RSocket { private final IntObjectMap senders; private final IntObjectMap> receivers; private final UnboundedProcessor sendProcessor; + private final int mtu; private final RequesterLeaseHandler leaseHandler; private final ByteBufAllocator allocator; private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; @@ -99,6 +101,7 @@ class RSocketRequester implements RSocket { PayloadDecoder payloadDecoder, Consumer errorConsumer, StreamIdSupplier streamIdSupplier, + int mtu, int keepAliveTickPeriod, int keepAliveAckTimeout, @Nullable KeepAliveHandler keepAliveHandler, @@ -108,6 +111,7 @@ class RSocketRequester implements RSocket { this.payloadDecoder = payloadDecoder; this.errorConsumer = errorConsumer; this.streamIdSupplier = streamIdSupplier; + this.mtu = mtu; this.leaseHandler = leaseHandler; this.senders = new SynchronizedIntObjectHashMap<>(); this.receivers = new SynchronizedIntObjectHashMap<>(); @@ -186,6 +190,11 @@ private Mono handleFireAndForget(Payload payload) { return Mono.error(err); } + if (!PayloadValidationUtils.isValid(this.mtu, payload)) { + payload.release(); + return Mono.error(new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); + } + final int streamId = streamIdSupplier.nextStreamId(receivers); return UnicastMonoEmpty.newInstance( @@ -210,6 +219,11 @@ private Mono handleRequestResponse(final Payload payload) { return Mono.error(err); } + if (!PayloadValidationUtils.isValid(this.mtu, payload)) { + payload.release(); + return Mono.error(new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); + } + int streamId = streamIdSupplier.nextStreamId(receivers); final UnboundedProcessor sendProcessor = this.sendProcessor; @@ -255,6 +269,11 @@ private Flux handleRequestStream(final Payload payload) { return Flux.error(err); } + if (!PayloadValidationUtils.isValid(this.mtu, payload)) { + payload.release(); + return Flux.error(new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); + } + int streamId = streamIdSupplier.nextStreamId(receivers); final UnboundedProcessor sendProcessor = this.sendProcessor; @@ -317,6 +336,13 @@ private Flux handleChannel(Flux request) { (s, flux) -> { Payload payload = s.get(); if (payload != null) { + if (!PayloadValidationUtils.isValid(mtu, payload)) { + payload.release(); + final IllegalArgumentException t = + new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); + errorConsumer.accept(t); + return Mono.error(t); + } return handleChannel(payload, flux); } else { return flux; @@ -348,6 +374,17 @@ protected void hookOnNext(Payload payload) { first = false; return; } + if (!PayloadValidationUtils.isValid(mtu, payload)) { + payload.release(); + cancel(); + final IllegalArgumentException t = + new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); + errorConsumer.accept(t); + // no need to send any errors. + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + receiver.onError(t); + return; + } final ByteBuf frame = PayloadFrameFlyweight.encode(allocator, streamId, false, false, true, payload); @@ -434,6 +471,11 @@ private Mono handleMetadataPush(Payload payload) { return Mono.error(err); } + if (!PayloadValidationUtils.isValid(this.mtu, payload)) { + payload.release(); + return Mono.error(new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); + } + return UnicastMonoEmpty.newInstance( () -> { ByteBuf metadataPushFrame = @@ -444,6 +486,7 @@ private Mono handleMetadataPush(Payload payload) { }); } + @Nullable private Throwable checkAvailable() { Throwable err = this.terminationError; if (err != null) { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index de6e8ad23..6f235587a 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -16,6 +16,8 @@ package io.rsocket.core; +import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; + import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.ReferenceCountUtil; @@ -51,6 +53,8 @@ class RSocketResponder implements ResponderRSocket { private final Consumer errorConsumer; private final ResponderLeaseHandler leaseHandler; + private final int mtu; + private final IntObjectMap sendingSubscriptions; private final IntObjectMap> channelProcessors; @@ -63,9 +67,11 @@ class RSocketResponder implements ResponderRSocket { RSocket requestHandler, PayloadDecoder payloadDecoder, Consumer errorConsumer, - ResponderLeaseHandler leaseHandler) { + ResponderLeaseHandler leaseHandler, + int mtu) { this.allocator = allocator; this.connection = connection; + this.mtu = mtu; this.requestHandler = requestHandler; this.responderRSocket = @@ -371,6 +377,15 @@ protected void hookOnNext(Payload payload) { isEmpty = false; } + if (!PayloadValidationUtils.isValid(mtu, payload)) { + payload.release(); + cancel(); + final IllegalArgumentException t = + new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); + handleError(streamId, t); + return; + } + ByteBuf byteBuf; try { byteBuf = PayloadFrameFlyweight.encodeNextComplete(allocator, streamId, payload); @@ -417,6 +432,15 @@ protected void hookOnSubscribe(Subscription s) { @Override protected void hookOnNext(Payload payload) { + if (!PayloadValidationUtils.isValid(mtu, payload)) { + payload.release(); + cancel(); + final IllegalArgumentException t = + new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); + handleError(streamId, t); + return; + } + ByteBuf byteBuf; try { byteBuf = PayloadFrameFlyweight.encodeNext(allocator, streamId, payload); diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java index d634f7374..e59ece86f 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java @@ -20,7 +20,14 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.util.ReferenceCountUtil; -import io.rsocket.frame.*; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.PayloadFrameFlyweight; +import io.rsocket.frame.RequestChannelFrameFlyweight; +import io.rsocket.frame.RequestFireAndForgetFrameFlyweight; +import io.rsocket.frame.RequestResponseFrameFlyweight; +import io.rsocket.frame.RequestStreamFrameFlyweight; import java.util.function.Consumer; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; diff --git a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java index 6cb05dec1..10725238a 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java @@ -61,6 +61,7 @@ static RSocketState requester(int tickPeriod, int timeout) { DefaultPayload::create, errors, StreamIdSupplier.clientSupplier(), + 0, tickPeriod, timeout, new DefaultKeepAliveHandler(connection), @@ -86,6 +87,7 @@ static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { DefaultPayload::create, errors, StreamIdSupplier.clientSupplier(), + 0, tickPeriod, timeout, new ResumableKeepAliveHandler(resumableConnection), diff --git a/rsocket-core/src/test/java/io/rsocket/core/PayloadValidationUtilsTest.java b/rsocket-core/src/test/java/io/rsocket/core/PayloadValidationUtilsTest.java new file mode 100644 index 000000000..e91fce848 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/PayloadValidationUtilsTest.java @@ -0,0 +1,99 @@ +package io.rsocket.core; + +import io.rsocket.Payload; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.util.DefaultPayload; +import java.util.concurrent.ThreadLocalRandom; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +class PayloadValidationUtilsTest { + + @Test + void shouldBeValidFrameWithNoFragmentation() { + byte[] data = + new byte + [FrameLengthFlyweight.FRAME_LENGTH_MASK + - FrameLengthFlyweight.FRAME_LENGTH_SIZE + - FrameHeaderFlyweight.size()]; + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data); + + Assertions.assertThat(PayloadValidationUtils.isValid(0, payload)).isTrue(); + } + + @Test + void shouldBeInValidFrameWithNoFragmentation() { + byte[] data = + new byte + [FrameLengthFlyweight.FRAME_LENGTH_MASK + - FrameLengthFlyweight.FRAME_LENGTH_SIZE + - FrameHeaderFlyweight.size() + + 1]; + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data); + + Assertions.assertThat(PayloadValidationUtils.isValid(0, payload)).isFalse(); + } + + @Test + void shouldBeValidFrameWithNoFragmentation0() { + byte[] metadata = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK / 2]; + byte[] data = + new byte + [FrameLengthFlyweight.FRAME_LENGTH_MASK / 2 + - FrameLengthFlyweight.FRAME_LENGTH_SIZE + - FrameHeaderFlyweight.size() + - FrameHeaderFlyweight.size()]; + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); + final Payload payload = DefaultPayload.create(data, metadata); + + Assertions.assertThat(PayloadValidationUtils.isValid(0, payload)).isTrue(); + } + + @Test + void shouldBeInValidFrameWithNoFragmentation1() { + byte[] metadata = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + byte[] data = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data, metadata); + + Assertions.assertThat(PayloadValidationUtils.isValid(0, payload)).isFalse(); + } + + @Test + void shouldBeValidFrameWithNoFragmentation2() { + byte[] metadata = new byte[1]; + byte[] data = new byte[1]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data, metadata); + + Assertions.assertThat(PayloadValidationUtils.isValid(0, payload)).isTrue(); + } + + @Test + void shouldBeValidFrameWithNoFragmentation3() { + byte[] metadata = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + byte[] data = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data, metadata); + + Assertions.assertThat(PayloadValidationUtils.isValid(64, payload)).isTrue(); + } + + @Test + void shouldBeValidFrameWithNoFragmentation4() { + byte[] metadata = new byte[1]; + byte[] data = new byte[1]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data, metadata); + + Assertions.assertThat(PayloadValidationUtils.isValid(64, payload)).isTrue(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java index 3cbb3c5d7..0a7f7a196 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -94,6 +94,7 @@ void setUp() { StreamIdSupplier.clientSupplier(), 0, 0, + 0, null, requesterLeaseHandler); @@ -111,7 +112,8 @@ void setUp() { mockRSocketHandler, payloadDecoder, err -> {}, - responderLeaseHandler); + responderLeaseHandler, + 0); } @Test diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index 8a2e114cc..8380290f2 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -67,6 +67,7 @@ void setUp() { StreamIdSupplier.clientSupplier(), 0, 0, + 0, null, RequesterLeaseHandler.None); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index 20b1825fa..101500da7 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -16,10 +16,21 @@ package io.rsocket.core; +import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; import static io.rsocket.frame.FrameHeaderFlyweight.frameType; -import static io.rsocket.frame.FrameType.*; +import static io.rsocket.frame.FrameType.CANCEL; +import static io.rsocket.frame.FrameType.KEEPALIVE; +import static io.rsocket.frame.FrameType.REQUEST_CHANNEL; +import static io.rsocket.frame.FrameType.REQUEST_RESPONSE; +import static io.rsocket.frame.FrameType.REQUEST_STREAM; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -27,9 +38,18 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.util.CharsetUtil; import io.rsocket.Payload; +import io.rsocket.RSocket; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.RejectedSetupException; -import io.rsocket.frame.*; +import io.rsocket.frame.CancelFrameFlyweight; +import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.PayloadFrameFlyweight; +import io.rsocket.frame.RequestChannelFrameFlyweight; +import io.rsocket.frame.RequestNFrameFlyweight; +import io.rsocket.frame.RequestStreamFrameFlyweight; import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.test.util.TestSubscriber; import io.rsocket.util.DefaultPayload; @@ -39,7 +59,10 @@ import java.util.ArrayList; import java.util.Iterator; import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.function.BiFunction; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.assertj.core.api.Assertions; import org.junit.Rule; import org.junit.Test; @@ -51,6 +74,7 @@ import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.UnicastProcessor; +import reactor.test.StepVerifier; public class RSocketRequesterTest { @@ -262,6 +286,62 @@ protected void hookOnSubscribe(Subscription subscription) {} Assertions.assertThat(iterator.hasNext()).isFalse(); } + @Test + public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmentation() { + prepareCalls() + .forEach( + generator -> { + byte[] metadata = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + byte[] data = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + StepVerifier.create( + generator.apply(rule.socket, DefaultPayload.create(data, metadata))) + .expectSubscription() + .expectErrorSatisfies( + t -> + Assertions.assertThat(t) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_PAYLOAD_ERROR_MESSAGE)) + .verify(); + }); + } + + @Test + public void + shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmentationForRequestChannelCase() { + byte[] metadata = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + byte[] data = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + StepVerifier.create( + rule.socket.requestChannel( + Flux.just(EmptyPayload.INSTANCE, DefaultPayload.create(data, metadata)))) + .expectSubscription() + .then( + () -> + rule.connection.addToReceivedBuffer( + RequestNFrameFlyweight.encode( + ByteBufAllocator.DEFAULT, + rule.getStreamIdForRequestType(REQUEST_CHANNEL), + 2))) + .expectErrorSatisfies( + t -> + Assertions.assertThat(t) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage(INVALID_PAYLOAD_ERROR_MESSAGE)) + .verify(); + } + + static Stream>> prepareCalls() { + return Stream.of( + RSocket::fireAndForget, + RSocket::requestResponse, + RSocket::requestStream, + (rSocket, payload) -> rSocket.requestChannel(Flux.just(payload)), + RSocket::metadataPush); + } + public int sendRequestResponse(Publisher response) { Subscriber sub = TestSubscriber.create(); response.subscribe(sub); @@ -285,6 +365,7 @@ protected RSocketRequester newRSocket() { StreamIdSupplier.clientSupplier(), 0, 0, + 0, null, RequesterLeaseHandler.None); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index 10157532a..5c147f46f 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -16,6 +16,7 @@ package io.rsocket.core; +import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; import static io.rsocket.frame.FrameHeaderFlyweight.frameType; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.*; @@ -34,11 +35,15 @@ import io.rsocket.util.EmptyPayload; import java.util.Collection; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; +import org.assertj.core.api.Assertions; import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; +import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class RSocketResponderTest { @@ -110,6 +115,58 @@ public Mono requestResponse(Payload payload) { assertThat("Subscription not cancelled.", cancelled.get(), is(true)); } + @Test + public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmentation() { + final int streamId = 4; + final AtomicBoolean cancelled = new AtomicBoolean(); + byte[] metadata = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + byte[] data = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + ThreadLocalRandom.current().nextBytes(metadata); + ThreadLocalRandom.current().nextBytes(data); + final Payload payload = DefaultPayload.create(data, metadata); + final AbstractRSocket acceptingSocket = + new AbstractRSocket() { + @Override + public Mono requestResponse(Payload p) { + return Mono.just(payload).doOnCancel(() -> cancelled.set(true)); + } + + @Override + public Flux requestStream(Payload p) { + return Flux.just(payload).doOnCancel(() -> cancelled.set(true)); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.just(payload).doOnCancel(() -> cancelled.set(true)); + } + }; + rule.setAcceptingSocket(acceptingSocket); + + final Runnable[] runnables = { + () -> rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE), + () -> rule.sendRequest(streamId, FrameType.REQUEST_STREAM), + () -> rule.sendRequest(streamId, FrameType.REQUEST_CHANNEL) + }; + + for (Runnable runnable : runnables) { + runnable.run(); + Assertions.assertThat(rule.errors) + .first() + .isInstanceOf(IllegalArgumentException.class) + .hasToString("java.lang.IllegalArgumentException: " + INVALID_PAYLOAD_ERROR_MESSAGE); + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> FrameHeaderFlyweight.frameType(bb) == FrameType.ERROR) + .matches(bb -> ErrorFrameFlyweight.dataUtf8(bb).contains(INVALID_PAYLOAD_ERROR_MESSAGE)); + + assertThat("Subscription not cancelled.", cancelled.get(), is(true)); + rule.init(); + rule.setAcceptingSocket(acceptingSocket); + } + } + public static class ServerSocketRule extends AbstractSocketRule { private RSocket acceptingSocket; @@ -151,7 +208,8 @@ protected RSocketResponder newRSocket() { acceptingSocket, DefaultPayload::create, throwable -> errors.add(throwable), - ResponderLeaseHandler.None); + ResponderLeaseHandler.None, + 0); } private void sendRequest(int streamId, FrameType frameType) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index b18fad890..edcc8971f 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -222,7 +222,8 @@ public Flux requestChannel(Publisher payloads) { requestAcceptor, DefaultPayload::create, throwable -> serverErrors.add(throwable), - ResponderLeaseHandler.None); + ResponderLeaseHandler.None, + 0); crs = new RSocketRequester( @@ -233,6 +234,7 @@ public Flux requestChannel(Publisher payloads) { StreamIdSupplier.clientSupplier(), 0, 0, + 0, null, RequesterLeaseHandler.None); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java index daab5d246..9344d69da 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -58,6 +58,7 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { StreamIdSupplier.clientSupplier(), 0, 0, + 0, null, RequesterLeaseHandler.None); @@ -93,6 +94,7 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { StreamIdSupplier.clientSupplier(), 0, 0, + 0, null, RequesterLeaseHandler.None); From a9c385aeb6cc6ba139da07859c391f86d77b2814 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sat, 11 Apr 2020 12:57:08 +0100 Subject: [PATCH 133/181] reduce package cycles (#774) * Remove package cycle between resume and internal Move ClientSetup and ServerSetup are closely associated with the RSocketFactory implementations. Moving them into core where they are used from breaks a package cycle between resume and internal. ClientSetup is straight-forward and folds easily into DefaultClientRSocketFactory. ConnectionUtil is closely associated with ServerSetup and has been folded into it. Signed-off-by: Rossen Stoyanchev * Remove package cycle between lease and exceptions MissingLeaseException is moved to the lease package next to the contracts it is declared in breaking a cycle between lease and exceptions. This is an API breakage but I the impact should be minor (none? the chances of something handling it explicitly) and hence an acceptable trade-off. Signed-off-by: Rossen Stoyanchev * Add RSocketProtocolException in top-level package Replaces (now deprecated) RSocketException from the exceptions package which can lead to cycles (e.g. with frame package). Other minor refinements: - error code validation - shared errorCode() implementation - error code in toString() - avoid NPE for null message, it's better to keep the original exception and null is allowed by getMessage(). Signed-off-by: Rossen Stoyanchev * Deprecate ErrorType This change deprecates ErrorType as the same constants are already declared in ErrorFrameFlyweight and there is hierarchy of exceptions with one exception per error code. Signed-off-by: Rossen Stoyanchev --- .../io/rsocket/RSocketErrorException.java | 82 +++++++++++++ .../core/DefaultClientRSocketFactory.java | 50 ++++---- .../core/DefaultServerRSocketFactory.java | 40 +++---- .../{internal => core}/ServerSetup.java | 48 ++++---- .../exceptions/ApplicationErrorException.java | 18 +-- .../rsocket/exceptions/CanceledException.java | 18 +-- .../exceptions/ConnectionCloseException.java | 18 +-- .../exceptions/ConnectionErrorException.java | 18 +-- .../exceptions/CustomRSocketException.java | 47 ++++---- .../io/rsocket/exceptions/Exceptions.java | 20 +++- .../rsocket/exceptions/InvalidException.java | 18 +-- .../exceptions/InvalidSetupException.java | 18 +-- .../rsocket/exceptions/RSocketException.java | 37 +++--- .../rsocket/exceptions/RejectedException.java | 18 +-- .../exceptions/RejectedResumeException.java | 18 +-- .../exceptions/RejectedSetupException.java | 18 +-- .../io/rsocket/exceptions/SetupException.java | 28 ++++- .../exceptions/UnsupportedSetupException.java | 18 +-- .../io/rsocket/frame/ErrorFrameFlyweight.java | 15 +-- .../main/java/io/rsocket/frame/ErrorType.java | 2 + .../java/io/rsocket/internal/ClientSetup.java | 112 ------------------ .../MissingLeaseException.java | 19 ++- .../rsocket/lease/RequesterLeaseHandler.java | 1 - .../rsocket/lease/ResponderLeaseHandler.java | 1 - .../java/io/rsocket/util/ConnectionUtils.java | 17 --- .../io/rsocket/core/RSocketLeaseTest.java | 2 +- .../java/io/rsocket/core/RSocketTest.java | 5 +- .../exceptions/RSocketExceptionTest.java | 13 +- 28 files changed, 329 insertions(+), 390 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/RSocketErrorException.java rename rsocket-core/src/main/java/io/rsocket/{internal => core}/ServerSetup.java (83%) delete mode 100644 rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java rename rsocket-core/src/main/java/io/rsocket/{exceptions => lease}/MissingLeaseException.java (60%) delete mode 100644 rsocket-core/src/main/java/io/rsocket/util/ConnectionUtils.java diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketErrorException.java b/rsocket-core/src/main/java/io/rsocket/RSocketErrorException.java new file mode 100644 index 000000000..b43b14bae --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/RSocketErrorException.java @@ -0,0 +1,82 @@ +/* + * 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; + +import reactor.util.annotation.Nullable; + +/** + * Exception that represents an RSocket protocol error. + * + * @see ERROR + * Frame (0x0B) + */ +public class RSocketErrorException extends RuntimeException { + + private static final long serialVersionUID = -1628781753426267554L; + + private static final int MIN_ERROR_CODE = 0x00000001; + + private static final int MAX_ERROR_CODE = 0xFFFFFFFE; + + private final int errorCode; + + /** + * Constructor with a protocol error code and a message. + * + * @param errorCode the RSocket protocol error code + * @param message error explanation + */ + public RSocketErrorException(int errorCode, String message) { + this(errorCode, message, null); + } + + /** + * Alternative to {@link #RSocketErrorException(int, String)} with a root cause. + * + * @param errorCode the RSocket protocol error code + * @param message error explanation + * @param cause a root cause for the error + */ + public RSocketErrorException(int errorCode, String message, @Nullable Throwable cause) { + super(message, cause); + this.errorCode = errorCode; + if (errorCode > MAX_ERROR_CODE && errorCode < MIN_ERROR_CODE) { + throw new IllegalArgumentException( + "Allowed errorCode value should be in range [0x00000001-0xFFFFFFFE]", this); + } + } + + /** + * Return the RSocket error code + * represented by this exception + * + * @return the RSocket protocol error code + */ + public int errorCode() { + return errorCode; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + " (0x" + + Integer.toHexString(errorCode) + + "): " + + getMessage(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java index b7cad7042..2ecde3663 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java @@ -17,6 +17,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; import io.rsocket.AbstractRSocket; import io.rsocket.ConnectionSetupPayload; import io.rsocket.DuplexConnection; @@ -28,7 +29,6 @@ import io.rsocket.frame.SetupFrameFlyweight; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.ClientServerInputMultiplexer; -import io.rsocket.internal.ClientSetup; import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.lease.LeaseStats; import io.rsocket.lease.Leases; @@ -39,6 +39,7 @@ import io.rsocket.plugins.Plugins; import io.rsocket.plugins.RSocketInterceptor; import io.rsocket.plugins.SocketAcceptorInterceptor; +import io.rsocket.resume.ClientRSocketSession; import io.rsocket.resume.ExponentialBackoffResumeStrategy; import io.rsocket.resume.InMemoryResumableFramesStore; import io.rsocket.resume.ResumableFramesStore; @@ -309,10 +310,31 @@ public Mono start() { return newConnection() .flatMap( connection -> { - ClientSetup clientSetup = clientSetup(connection); - ByteBuf resumeToken = clientSetup.resumeToken(); - KeepAliveHandler keepAliveHandler = clientSetup.keepAliveHandler(); - DuplexConnection wrappedConnection = clientSetup.connection(); + ByteBuf resumeToken; + KeepAliveHandler keepAliveHandler; + DuplexConnection wrappedConnection; + + if (resumeEnabled) { + resumeToken = resumeTokenSupplier.get(); + ClientRSocketSession session = + new ClientRSocketSession( + connection, + allocator, + resumeSessionDuration, + resumeStrategySupplier, + resumeStoreFactory.apply(resumeToken), + resumeStreamTimeout, + resumeCleanupStoreOnKeepAlive) + .continueWith(newConnection()) + .resumeToken(resumeToken); + keepAliveHandler = + new KeepAliveHandler.ResumableKeepAliveHandler(session.resumableConnection()); + wrappedConnection = session.resumableConnection(); + } else { + resumeToken = Unpooled.EMPTY_BUFFER; + keepAliveHandler = new KeepAliveHandler.DefaultKeepAliveHandler(connection); + wrappedConnection = connection; + } ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(wrappedConnection, plugins, true); @@ -407,24 +429,6 @@ private int keepAliveTimeout() { return (int) (ackTimeout.toMillis() + tickPeriod.toMillis() * missedAcks); } - private ClientSetup clientSetup(DuplexConnection startConnection) { - if (resumeEnabled) { - ByteBuf resumeToken = resumeTokenSupplier.get(); - return new ClientSetup.ResumableClientSetup( - allocator, - startConnection, - newConnection(), - resumeToken, - resumeStoreFactory.apply(resumeToken), - resumeSessionDuration, - resumeStreamTimeout, - resumeStrategySupplier, - resumeCleanupStoreOnKeepAlive); - } else { - return new ClientSetup.DefaultClientSetup(startConnection); - } - } - private Mono newConnection() { return Mono.fromSupplier(transportClient).flatMap(t -> t.connect(mtu)); } diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java index 85543181a..73d3513da 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java @@ -29,7 +29,6 @@ import io.rsocket.frame.SetupFrameFlyweight; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.ClientServerInputMultiplexer; -import io.rsocket.internal.ServerSetup; import io.rsocket.lease.Leases; import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.lease.ResponderLeaseHandler; @@ -42,7 +41,6 @@ import io.rsocket.resume.ResumableFramesStore; import io.rsocket.resume.SessionManager; import io.rsocket.transport.ServerTransport; -import io.rsocket.util.ConnectionUtils; import io.rsocket.util.MultiSubscriberRSocket; import java.time.Duration; import java.util.Objects; @@ -232,7 +230,16 @@ private Mono accept( case RESUME: return acceptResume(serverSetup, startFrame, multiplexer); default: - return acceptUnknown(startFrame, multiplexer); + return serverSetup + .sendError( + multiplexer, + new InvalidSetupException( + "invalid setup frame: " + FrameHeaderFlyweight.frameType(startFrame))) + .doFinally( + signalType -> { + startFrame.release(); + multiplexer.dispose(); + }); } } @@ -240,7 +247,8 @@ private Mono acceptSetup( ServerSetup serverSetup, ByteBuf setupFrame, ClientServerInputMultiplexer multiplexer) { if (!SetupFrameFlyweight.isSupportedVersion(setupFrame)) { - return sendError( + return serverSetup + .sendError( multiplexer, new InvalidSetupException( "Unsupported version: " + SetupFrameFlyweight.humanReadableVersion(setupFrame))) @@ -254,7 +262,8 @@ private Mono acceptSetup( boolean isLeaseEnabled = leaseEnabled; if (SetupFrameFlyweight.honorLease(setupFrame) && !isLeaseEnabled) { - return sendError(multiplexer, new InvalidSetupException("lease is not supported")) + return serverSetup + .sendError(multiplexer, new InvalidSetupException("lease is not supported")) .doFinally( signalType -> { setupFrame.release(); @@ -296,7 +305,10 @@ private Mono acceptSetup( .applySocketAcceptorInterceptor(acceptor) .accept(setupPayload, wrappedRSocketRequester) .onErrorResume( - err -> sendError(multiplexer, rejectedSetupError(err)).then(Mono.error(err))) + err -> + serverSetup + .sendError(multiplexer, rejectedSetupError(err)) + .then(Mono.error(err))) .doOnNext( rSocketHandler -> { RSocket wrappedRSocketHandler = plugins.applyResponder(rSocketHandler); @@ -357,22 +369,6 @@ private ServerSetup serverSetup() { : new ServerSetup.DefaultServerSetup(allocator); } - private Mono acceptUnknown(ByteBuf frame, ClientServerInputMultiplexer multiplexer) { - return sendError( - multiplexer, - new InvalidSetupException( - "invalid setup frame: " + FrameHeaderFlyweight.frameType(frame))) - .doFinally( - signalType -> { - frame.release(); - multiplexer.dispose(); - }); - } - - private Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { - return ConnectionUtils.sendError(allocator, multiplexer, exception); - } - private Exception rejectedSetupError(Throwable err) { String msg = err.getMessage(); return new RejectedSetupException(msg == null ? "rejected by server acceptor" : msg); diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java similarity index 83% rename from rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java rename to rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java index dbd8bc173..16f5f61b1 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package io.rsocket.internal; +package io.rsocket.core; import static io.rsocket.keepalive.KeepAliveHandler.*; @@ -22,32 +22,45 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.exceptions.RejectedResumeException; import io.rsocket.exceptions.UnsupportedSetupException; +import io.rsocket.frame.ErrorFrameFlyweight; import io.rsocket.frame.ResumeFrameFlyweight; import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.internal.ClientServerInputMultiplexer; import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.resume.*; -import io.rsocket.util.ConnectionUtils; import java.time.Duration; import java.util.function.BiFunction; import java.util.function.Function; import reactor.core.publisher.Mono; -public interface ServerSetup { +abstract class ServerSetup { - Mono acceptRSocketSetup( + final ByteBufAllocator allocator; + + public ServerSetup(ByteBufAllocator allocator) { + this.allocator = allocator; + } + + abstract Mono acceptRSocketSetup( ByteBuf frame, ClientServerInputMultiplexer multiplexer, BiFunction> then); - Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer); + abstract Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer); - default void dispose() {} + void dispose() {} - class DefaultServerSetup implements ServerSetup { - private final ByteBufAllocator allocator; + Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { + return multiplexer + .asSetupConnection() + .sendOne(ErrorFrameFlyweight.encode(allocator, 0, exception)) + .onErrorResume(err -> Mono.empty()); + } - public DefaultServerSetup(ByteBufAllocator allocator) { - this.allocator = allocator; + static class DefaultServerSetup extends ServerSetup { + + DefaultServerSetup(ByteBufAllocator allocator) { + super(allocator); } @Override @@ -78,13 +91,9 @@ public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexe multiplexer.dispose(); }); } - - private Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { - return ConnectionUtils.sendError(allocator, multiplexer, exception); - } } - class ResumableServerSetup implements ServerSetup { + static class ResumableServerSetup extends ServerSetup { private final ByteBufAllocator allocator; private final SessionManager sessionManager; private final Duration resumeSessionDuration; @@ -92,13 +101,14 @@ class ResumableServerSetup implements ServerSetup { private final Function resumeStoreFactory; private final boolean cleanupStoreOnKeepAlive; - public ResumableServerSetup( + ResumableServerSetup( ByteBufAllocator allocator, SessionManager sessionManager, Duration resumeSessionDuration, Duration resumeStreamTimeout, Function resumeStoreFactory, boolean cleanupStoreOnKeepAlive) { + super(allocator); this.allocator = allocator; this.sessionManager = sessionManager; this.resumeSessionDuration = resumeSessionDuration; @@ -155,10 +165,6 @@ public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexe } } - private Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { - return ConnectionUtils.sendError(allocator, multiplexer, exception); - } - @Override public void dispose() { sessionManager.dispose(); diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java index e92534b2a..351e045a3 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorType; +import io.rsocket.frame.ErrorFrameFlyweight; +import javax.annotation.Nullable; /** * Application layer logic generating a Reactive Streams {@code onError} event. @@ -32,10 +33,9 @@ public final class ApplicationErrorException extends RSocketException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public ApplicationErrorException(String message) { - super(message); + this(message, null); } /** @@ -43,14 +43,8 @@ public ApplicationErrorException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public ApplicationErrorException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.APPLICATION_ERROR; + public ApplicationErrorException(String message, @Nullable Throwable cause) { + super(ErrorFrameFlyweight.APPLICATION_ERROR, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java index 984e8249b..537cf2bf2 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorType; +import io.rsocket.frame.ErrorFrameFlyweight; +import javax.annotation.Nullable; /** * The Responder canceled the request but may have started processing it (similar to REJECTED but @@ -33,10 +34,9 @@ public final class CanceledException extends RSocketException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public CanceledException(String message) { - super(message); + this(message, null); } /** @@ -44,14 +44,8 @@ public CanceledException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public CanceledException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.CANCELED; + public CanceledException(String message, @Nullable Throwable cause) { + super(ErrorFrameFlyweight.CANCELED, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java index 3f4f4309d..f1f1a47d8 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorType; +import io.rsocket.frame.ErrorFrameFlyweight; +import javax.annotation.Nullable; /** * The connection is being terminated. Sender or Receiver of this frame MUST wait for outstanding @@ -33,10 +34,9 @@ public final class ConnectionCloseException extends RSocketException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public ConnectionCloseException(String message) { - super(message); + this(message, null); } /** @@ -44,14 +44,8 @@ public ConnectionCloseException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public ConnectionCloseException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.CONNECTION_CLOSE; + public ConnectionCloseException(String message, @Nullable Throwable cause) { + super(ErrorFrameFlyweight.CONNECTION_CLOSE, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java index beaa3d0d0..9581cfc97 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorType; +import io.rsocket.frame.ErrorFrameFlyweight; +import javax.annotation.Nullable; /** * The connection is being terminated. Sender or Receiver of this frame MAY close the connection @@ -33,10 +34,9 @@ public final class ConnectionErrorException extends RSocketException implements * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public ConnectionErrorException(String message) { - super(message); + this(message, null); } /** @@ -44,14 +44,8 @@ public ConnectionErrorException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public ConnectionErrorException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.CONNECTION_ERROR; + public ConnectionErrorException(String message, @Nullable Throwable cause) { + super(ErrorFrameFlyweight.CONNECTION_ERROR, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java index 6315206b5..5c1154ebd 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java @@ -1,28 +1,36 @@ +/* + * 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.exceptions; -import io.rsocket.frame.ErrorType; +import io.rsocket.frame.ErrorFrameFlyweight; +import javax.annotation.Nullable; public class CustomRSocketException extends RSocketException { private static final long serialVersionUID = 7873267740343446585L; - private final int errorCode; - /** * Constructs a new exception with the specified message. * * @param errorCode customizable error code. Should be in range [0x00000301-0xFFFFFFFE] * @param message the message - * @throws NullPointerException if {@code message} is {@code null} * @throws IllegalArgumentException if {@code errorCode} is out of allowed range */ public CustomRSocketException(int errorCode, String message) { - super(message); - if (errorCode > ErrorType.MAX_USER_ALLOWED_ERROR_CODE - && errorCode < ErrorType.MIN_USER_ALLOWED_ERROR_CODE) { - throw new IllegalArgumentException( - "Allowed errorCode value should be in range [0x00000301-0xFFFFFFFE]"); - } - this.errorCode = errorCode; + this(errorCode, message, null); } /** @@ -31,21 +39,14 @@ public CustomRSocketException(int errorCode, String message) { * @param errorCode customizable error code. Should be in range [0x00000301-0xFFFFFFFE] * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} * @throws IllegalArgumentException if {@code errorCode} is out of allowed range */ - public CustomRSocketException(int errorCode, String message, Throwable cause) { - super(message, cause); - if (errorCode > ErrorType.MAX_USER_ALLOWED_ERROR_CODE - && errorCode < ErrorType.MIN_USER_ALLOWED_ERROR_CODE) { + public CustomRSocketException(int errorCode, String message, @Nullable Throwable cause) { + super(errorCode, message, cause); + if (errorCode > ErrorFrameFlyweight.MAX_USER_ALLOWED_ERROR_CODE + && errorCode < ErrorFrameFlyweight.MIN_USER_ALLOWED_ERROR_CODE) { throw new IllegalArgumentException( - "Allowed errorCode value should be in range [0x00000301-0xFFFFFFFE]"); + "Allowed errorCode value should be in range [0x00000301-0xFFFFFFFE]", this); } - this.errorCode = errorCode; - } - - @Override - public int errorCode() { - return errorCode; } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java b/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java index 3a10410f0..fe2d304f5 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,9 +16,21 @@ package io.rsocket.exceptions; -import static io.rsocket.frame.ErrorFrameFlyweight.*; +import static io.rsocket.frame.ErrorFrameFlyweight.APPLICATION_ERROR; +import static io.rsocket.frame.ErrorFrameFlyweight.CANCELED; +import static io.rsocket.frame.ErrorFrameFlyweight.CONNECTION_CLOSE; +import static io.rsocket.frame.ErrorFrameFlyweight.CONNECTION_ERROR; +import static io.rsocket.frame.ErrorFrameFlyweight.INVALID; +import static io.rsocket.frame.ErrorFrameFlyweight.INVALID_SETUP; +import static io.rsocket.frame.ErrorFrameFlyweight.MAX_USER_ALLOWED_ERROR_CODE; +import static io.rsocket.frame.ErrorFrameFlyweight.MIN_USER_ALLOWED_ERROR_CODE; +import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED; +import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED_RESUME; +import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED_SETUP; +import static io.rsocket.frame.ErrorFrameFlyweight.UNSUPPORTED_SETUP; import io.netty.buffer.ByteBuf; +import io.rsocket.RSocketErrorException; import io.rsocket.frame.ErrorFrameFlyweight; import java.util.Objects; @@ -28,10 +40,10 @@ public final class Exceptions { private Exceptions() {} /** - * Create a {@link RSocketException} from a Frame that matches the error code it contains. + * Create a {@link RSocketErrorException} from a Frame that matches the error code it contains. * * @param frame the frame to retrieve the error code and message from - * @return a {@link RSocketException} that matches the error code in the Frame + * @return a {@link RSocketErrorException} that matches the error code in the Frame * @throws NullPointerException if {@code frame} is {@code null} */ public static RuntimeException from(int streamId, ByteBuf frame) { diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java index 4783b1590..a4b28659f 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorType; +import io.rsocket.frame.ErrorFrameFlyweight; +import javax.annotation.Nullable; /** * The request is invalid. @@ -32,10 +33,9 @@ public final class InvalidException extends RSocketException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public InvalidException(String message) { - super(message); + this(message, null); } /** @@ -43,14 +43,8 @@ public InvalidException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public InvalidException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.INVALID; + public InvalidException(String message, @Nullable Throwable cause) { + super(ErrorFrameFlyweight.INVALID, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java index b3705d5b7..1ff53d51d 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorType; +import io.rsocket.frame.ErrorFrameFlyweight; +import javax.annotation.Nullable; /** * The Setup frame is invalid for the server (it could be that the client is too recent for the old @@ -33,10 +34,9 @@ public final class InvalidSetupException extends SetupException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public InvalidSetupException(String message) { - super(message); + this(message, null); } /** @@ -44,14 +44,8 @@ public InvalidSetupException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public InvalidSetupException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.INVALID_SETUP; + public InvalidSetupException(String message, @Nullable Throwable cause) { + super(ErrorFrameFlyweight.INVALID_SETUP, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RSocketException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RSocketException.java index 7508a1ee3..93c49d5e2 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RSocketException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RSocketException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,41 +16,48 @@ package io.rsocket.exceptions; -import java.util.Objects; +import io.rsocket.RSocketErrorException; +import io.rsocket.frame.ErrorFrameFlyweight; import reactor.util.annotation.Nullable; -/** The root of the RSocket exception hierarchy. */ -public abstract class RSocketException extends RuntimeException { +/** + * The root of the RSocket exception hierarchy. + * + * @deprecated please use {@link RSocketErrorException} instead + */ +@Deprecated +public abstract class RSocketException extends RSocketErrorException { private static final long serialVersionUID = 2912815394105575423L; /** - * Constructs a new exception with the specified message. + * Constructs a new exception with the specified message and error code 0x201 (Application error). * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public RSocketException(String message) { - super(Objects.requireNonNull(message, "message must not be null")); + this(message, null); } /** - * Constructs a new exception with the specified message and cause. + * Constructs a new exception with the specified message and cause and error code 0x201 + * (Application error). * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} is {@code null} */ public RSocketException(String message, @Nullable Throwable cause) { - super(Objects.requireNonNull(message, "message must not be null"), cause); + super(ErrorFrameFlyweight.APPLICATION_ERROR, message, cause); } /** - * Returns the RSocket error code - * represented by this exception + * Constructs a new exception with the specified error code, message and cause. * - * @return the RSocket error code + * @param errorCode the RSocket protocol error code + * @param message the message + * @param cause the cause of this exception */ - public abstract int errorCode(); + public RSocketException(int errorCode, String message, @Nullable Throwable cause) { + super(errorCode, message, cause); + } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java index 4ab83182e..3fad3f396 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorType; +import io.rsocket.frame.ErrorFrameFlyweight; +import javax.annotation.Nullable; /** * Despite being a valid request, the Responder decided to reject it. The Responder guarantees that @@ -34,10 +35,9 @@ public class RejectedException extends RSocketException implements Retryable { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public RejectedException(String message) { - super(message); + this(message, null); } /** @@ -45,14 +45,8 @@ public RejectedException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public RejectedException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.REJECTED; + public RejectedException(String message, @Nullable Throwable cause) { + super(ErrorFrameFlyweight.REJECTED, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java index 0d4116538..a10eb4197 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorType; +import io.rsocket.frame.ErrorFrameFlyweight; +import javax.annotation.Nullable; /** * The server rejected the resume, it can specify the reason in the payload. @@ -32,10 +33,9 @@ public final class RejectedResumeException extends RSocketException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public RejectedResumeException(String message) { - super(message); + this(message, null); } /** @@ -43,14 +43,8 @@ public RejectedResumeException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public RejectedResumeException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.REJECTED_RESUME; + public RejectedResumeException(String message, @Nullable Throwable cause) { + super(ErrorFrameFlyweight.REJECTED_RESUME, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java index 1fa5f604e..6b5dc0f8b 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorType; +import io.rsocket.frame.ErrorFrameFlyweight; +import javax.annotation.Nullable; /** * The server rejected the setup, it can specify the reason in the payload. @@ -32,10 +33,9 @@ public final class RejectedSetupException extends SetupException implements Retr * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public RejectedSetupException(String message) { - super(message); + this(message, null); } /** @@ -43,14 +43,8 @@ public RejectedSetupException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public RejectedSetupException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.REJECTED_SETUP; + public RejectedSetupException(String message, @Nullable Throwable cause) { + super(ErrorFrameFlyweight.REJECTED_SETUP, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java index 2111a51b1..712508f0b 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,6 +16,9 @@ package io.rsocket.exceptions; +import io.rsocket.frame.ErrorFrameFlyweight; +import javax.annotation.Nullable; + /** The root of the setup exception hierarchy. */ public abstract class SetupException extends RSocketException { @@ -25,10 +28,11 @@ public abstract class SetupException extends RSocketException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} + * @deprecated please use {@link #SetupException(int, String, Throwable)} */ + @Deprecated public SetupException(String message) { - super(message); + this(message, null); } /** @@ -36,9 +40,21 @@ public SetupException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} + * @deprecated please use {@link #SetupException(int, String, Throwable)} + */ + @Deprecated + public SetupException(String message, @Nullable Throwable cause) { + this(ErrorFrameFlyweight.INVALID_SETUP, message, cause); + } + + /** + * Constructs a new exception with the specified error code, message and cause. + * + * @param errorCode the RSocket protocol code + * @param message the message + * @param cause the cause of this exception */ - public SetupException(String message, Throwable cause) { - super(message, cause); + public SetupException(int errorCode, String message, @Nullable Throwable cause) { + super(errorCode, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java index 7d14bc5d2..b112b95be 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,7 +16,8 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorType; +import io.rsocket.frame.ErrorFrameFlyweight; +import javax.annotation.Nullable; /** * Some (or all) of the parameters specified by the client are unsupported by the server. @@ -32,10 +33,9 @@ public final class UnsupportedSetupException extends SetupException { * Constructs a new exception with the specified message. * * @param message the message - * @throws NullPointerException if {@code message} is {@code null} */ public UnsupportedSetupException(String message) { - super(message); + this(message, null); } /** @@ -43,14 +43,8 @@ public UnsupportedSetupException(String message) { * * @param message the message * @param cause the cause of this exception - * @throws NullPointerException if {@code message} or {@code cause} is {@code null} */ - public UnsupportedSetupException(String message, Throwable cause) { - super(message, cause); - } - - @Override - public int errorCode() { - return ErrorType.UNSUPPORTED_SETUP; + public UnsupportedSetupException(String message, @Nullable Throwable cause) { + super(ErrorFrameFlyweight.UNSUPPORTED_SETUP, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java index df9d39ba8..ab26233f1 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java @@ -3,7 +3,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; -import io.rsocket.exceptions.RSocketException; +import io.rsocket.RSocketErrorException; import java.nio.charset.StandardCharsets; public class ErrorFrameFlyweight { @@ -28,7 +28,10 @@ public static ByteBuf encode( ByteBufAllocator allocator, int streamId, Throwable t, ByteBuf data) { ByteBuf header = FrameHeaderFlyweight.encode(allocator, streamId, FrameType.ERROR, 0); - int errorCode = errorCodeFromException(t); + int errorCode = + t instanceof RSocketErrorException + ? ((RSocketErrorException) t).errorCode() + : APPLICATION_ERROR; header.writeInt(errorCode); @@ -41,14 +44,6 @@ public static ByteBuf encode(ByteBufAllocator allocator, int streamId, Throwable return encode(allocator, streamId, t, data); } - public static int errorCodeFromException(Throwable t) { - if (t instanceof RSocketException) { - return ((RSocketException) t).errorCode(); - } - - return APPLICATION_ERROR; - } - public static int errorCode(ByteBuf byteBuf) { byteBuf.markReaderIndex(); byteBuf.skipBytes(FrameHeaderFlyweight.size()); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ErrorType.java b/rsocket-core/src/main/java/io/rsocket/frame/ErrorType.java index ffd99930d..b41a5d59e 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/ErrorType.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/ErrorType.java @@ -5,7 +5,9 @@ * * @see Error * Codes + * @deprecated please use constants in {@link ErrorFrameFlyweight}. */ +@Deprecated public final class ErrorType { /** diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java deleted file mode 100644 index 38217bdc2..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientSetup.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2015-2019 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.internal; - -import static io.rsocket.keepalive.KeepAliveHandler.*; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.rsocket.DuplexConnection; -import io.rsocket.keepalive.KeepAliveHandler; -import io.rsocket.resume.ClientRSocketSession; -import io.rsocket.resume.ResumableDuplexConnection; -import io.rsocket.resume.ResumableFramesStore; -import io.rsocket.resume.ResumeStrategy; -import java.time.Duration; -import java.util.function.Supplier; -import reactor.core.publisher.Mono; - -public interface ClientSetup { - - DuplexConnection connection(); - - KeepAliveHandler keepAliveHandler(); - - ByteBuf resumeToken(); - - class DefaultClientSetup implements ClientSetup { - private final DuplexConnection connection; - - public DefaultClientSetup(DuplexConnection connection) { - this.connection = connection; - } - - @Override - public DuplexConnection connection() { - return connection; - } - - @Override - public KeepAliveHandler keepAliveHandler() { - return new DefaultKeepAliveHandler(connection); - } - - @Override - public ByteBuf resumeToken() { - return Unpooled.EMPTY_BUFFER; - } - } - - class ResumableClientSetup implements ClientSetup { - private final ByteBuf resumeToken; - private final ResumableDuplexConnection duplexConnection; - private final ResumableKeepAliveHandler keepAliveHandler; - - public ResumableClientSetup( - ByteBufAllocator allocator, - DuplexConnection connection, - Mono newConnectionFactory, - ByteBuf resumeToken, - ResumableFramesStore resumableFramesStore, - Duration resumeSessionDuration, - Duration resumeStreamTimeout, - Supplier resumeStrategySupplier, - boolean cleanupStoreOnKeepAlive) { - - ClientRSocketSession rSocketSession = - new ClientRSocketSession( - connection, - allocator, - resumeSessionDuration, - resumeStrategySupplier, - resumableFramesStore, - resumeStreamTimeout, - cleanupStoreOnKeepAlive) - .continueWith(newConnectionFactory) - .resumeToken(resumeToken); - this.duplexConnection = rSocketSession.resumableConnection(); - this.keepAliveHandler = new ResumableKeepAliveHandler(duplexConnection); - this.resumeToken = resumeToken; - } - - @Override - public DuplexConnection connection() { - return duplexConnection; - } - - @Override - public KeepAliveHandler keepAliveHandler() { - return keepAliveHandler; - } - - @Override - public ByteBuf resumeToken() { - return resumeToken; - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/MissingLeaseException.java b/rsocket-core/src/main/java/io/rsocket/lease/MissingLeaseException.java similarity index 60% rename from rsocket-core/src/main/java/io/rsocket/exceptions/MissingLeaseException.java rename to rsocket-core/src/main/java/io/rsocket/lease/MissingLeaseException.java index 4bd6ffb99..734d16d07 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/MissingLeaseException.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/MissingLeaseException.java @@ -1,6 +1,21 @@ -package io.rsocket.exceptions; +/* + * 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.lease; -import io.rsocket.lease.Lease; +import io.rsocket.exceptions.RejectedException; import java.util.Objects; import javax.annotation.Nonnull; import javax.annotation.Nullable; diff --git a/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java b/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java index ca2111e87..dd4247090 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java @@ -18,7 +18,6 @@ import io.netty.buffer.ByteBuf; import io.rsocket.Availability; -import io.rsocket.exceptions.MissingLeaseException; import io.rsocket.frame.LeaseFrameFlyweight; import java.util.function.Consumer; import reactor.core.Disposable; diff --git a/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java b/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java index c517a55c4..5ca745ee7 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java @@ -19,7 +19,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.Availability; -import io.rsocket.exceptions.MissingLeaseException; import io.rsocket.frame.LeaseFrameFlyweight; import java.util.Optional; import java.util.function.Consumer; diff --git a/rsocket-core/src/main/java/io/rsocket/util/ConnectionUtils.java b/rsocket-core/src/main/java/io/rsocket/util/ConnectionUtils.java deleted file mode 100644 index dd8bbf907..000000000 --- a/rsocket-core/src/main/java/io/rsocket/util/ConnectionUtils.java +++ /dev/null @@ -1,17 +0,0 @@ -package io.rsocket.util; - -import io.netty.buffer.ByteBufAllocator; -import io.rsocket.frame.ErrorFrameFlyweight; -import io.rsocket.internal.ClientServerInputMultiplexer; -import reactor.core.publisher.Mono; - -public class ConnectionUtils { - - public static Mono sendError( - ByteBufAllocator allocator, ClientServerInputMultiplexer multiplexer, Exception exception) { - return multiplexer - .asSetupConnection() - .sendOne(ErrorFrameFlyweight.encode(allocator, 0, exception)) - .onErrorResume(err -> Mono.empty()); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java index 0a7f7a196..a9076428c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -27,7 +27,6 @@ import io.netty.buffer.UnpooledByteBufAllocator; import io.rsocket.*; import io.rsocket.exceptions.Exceptions; -import io.rsocket.exceptions.MissingLeaseException; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; import io.rsocket.frame.LeaseFrameFlyweight; @@ -35,6 +34,7 @@ import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.ClientServerInputMultiplexer; import io.rsocket.lease.*; +import io.rsocket.lease.MissingLeaseException; import io.rsocket.plugins.PluginRegistry; import io.rsocket.test.util.TestClientTransport; import io.rsocket.test.util.TestDuplexConnection; diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index edcc8971f..376614070 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -117,8 +117,7 @@ public Mono requestResponse(Payload payload) { // Client sees error through normal API rule.assertNoClientErrors(); - rule.assertServerError( - "io.rsocket.exceptions.CustomRSocketException: Deliberate Custom exception."); + rule.assertServerError("CustomRSocketException (0x501): Deliberate Custom exception."); } @Test(timeout = 2000) diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java index 8c39e8250..ccf7649d2 100644 --- a/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/RSocketExceptionTest.java @@ -17,27 +17,22 @@ package io.rsocket.exceptions; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; interface RSocketExceptionTest { - @DisplayName("constructor throws NullPointerException with null message") + @DisplayName("constructor does not throw NullPointerException with null message") @Test default void constructorWithNullMessage() { - assertThatNullPointerException() - .isThrownBy(() -> getException(null)) - .withMessage("message must not be null"); + assertThat(getException(null)).hasMessage(null); } - @DisplayName("constructor throws NullPointerException with null message and cause") + @DisplayName("constructor does not throw NullPointerException with null message and cause") @Test default void constructorWithNullMessageAndCause() { - assertThatNullPointerException() - .isThrownBy(() -> getException(null, new Exception())) - .withMessage("message must not be null"); + assertThat(getException(null)).hasMessage(null); } @DisplayName("errorCode returns specified value") From 0ac54d4bf41a66363648ca89b6e397b90bd4dd12 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 15 Apr 2020 17:56:37 +0100 Subject: [PATCH 134/181] RSocketFactory API enhancements (#780) This commit adds RSocketConnector and RSocketServer (io.rsocket.core) as replacements for the now deprecated RSocketFactory (io.rsocket). A key goal for the change is to avoid a class in the top-level package with references to sub-packages which leads to package cycles, but also an opportunity to review and improve the API. Closes gh-778 Signed-off-by: Rossen Stoyanchev --- README.md | 31 +- .../main/java/io/rsocket/RSocketFactory.java | 402 ++++++++++++---- .../core/DefaultClientRSocketFactory.java | 436 ------------------ .../core/DefaultServerRSocketFactory.java | 377 --------------- .../io/rsocket/core/RSocketConnector.java | 375 +++++++++++++++ .../java/io/rsocket/core/RSocketServer.java | 299 ++++++++++++ .../src/main/java/io/rsocket/core/Resume.java | 96 ++++ .../ClientServerInputMultiplexer.java | 17 +- .../InitializingInterceptorRegistry.java | 52 +++ .../rsocket/plugins/InterceptorRegistry.java | 83 ++++ .../io/rsocket/plugins/PluginRegistry.java | 111 ----- .../main/java/io/rsocket/plugins/Plugins.java | 40 -- .../io/rsocket/core/RSocketLeaseTest.java | 22 +- .../io/rsocket/core/RSocketReconnectTest.java | 41 +- .../core/RSocketServerFragmentationTest.java | 43 ++ .../io/rsocket/core/SetupRejectionTest.java | 2 +- .../ClientServerInputMultiplexerTest.java | 8 +- .../tcp/channel/ChannelEchoClient.java | 64 +-- .../transport/tcp/duplex/DuplexClient.java | 37 +- .../transport/tcp/lease/LeaseExample.java | 33 +- .../tcp/requestresponse/HelloWorldClient.java | 16 +- .../tcp/resume/ResumeFileTransfer.java | 44 +- .../transport/tcp/stream/StreamingClient.java | 21 +- .../transport/ws/WebSocketHeadersSample.java | 29 +- .../rsocket/integration/IntegrationTest.java | 72 ++- .../integration/InteractionsLoadTest.java | 20 +- .../integration/TcpIntegrationTest.java | 15 +- .../rsocket/integration/TestingStreaming.java | 153 +++--- .../rsocket/resume/ResumeIntegrationTest.java | 65 ++- .../java/io/rsocket/test/ClientSetupRule.java | 15 +- .../java/io/rsocket/test/TransportTest.java | 16 +- .../transport/local/LocalPingPong.java | 20 +- .../io/rsocket/integration/FragmentTest.java | 16 +- ...actoryNettyTransportFragmentationTest.java | 91 +--- .../transport/netty/SetupRejectionTest.java | 15 +- .../io/rsocket/transport/netty/TcpPing.java | 16 +- .../transport/netty/TcpPongServer.java | 15 +- .../WebSocketTransportIntegrationTest.java | 12 +- .../transport/netty/WebsocketPing.java | 11 +- .../WebsocketPingPongIntegrationTest.java | 18 +- .../transport/netty/WebsocketPongServer.java | 12 +- 41 files changed, 1661 insertions(+), 1600 deletions(-) delete mode 100644 rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java create mode 100644 rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java create mode 100644 rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java create mode 100644 rsocket-core/src/main/java/io/rsocket/core/Resume.java create mode 100644 rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java create mode 100644 rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/plugins/Plugins.java create mode 100644 rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java diff --git a/README.md b/README.md index 173c3e1ad..bb427cc35 100644 --- a/README.md +++ b/README.md @@ -23,8 +23,8 @@ Example: ```groovy dependencies { - implementation 'io.rsocket:rsocket-core:1.0.0-RC3' - implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC3' + implementation 'io.rsocket:rsocket-core:1.0.0-RC6' + implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC6' // implementation 'io.rsocket:rsocket-core:1.0.0-RC4-SNAPSHOT' // implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC4-SNAPSHOT' } @@ -57,7 +57,7 @@ package io.rsocket.transport.netty; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; import io.rsocket.transport.netty.client.WebsocketClientTransport; import io.rsocket.util.DefaultPayload; import reactor.core.publisher.Flux; @@ -67,14 +67,14 @@ import java.net.URI; public class ExampleClient { public static void main(String[] args) { WebsocketClientTransport ws = WebsocketClientTransport.create(URI.create("ws://rsocket-demo.herokuapp.com/ws")); - RSocket client = RSocketFactory.connect().keepAlive().transport(ws).start().block(); + RSocket clientRSocket = RSocketConnector.connectWith(ws).block(); try { - Flux s = client.requestStream(DefaultPayload.create("peace")); + Flux s = clientRSocket.requestStream(DefaultPayload.create("peace")); s.take(10).doOnNext(p -> System.out.println(p.getDataUtf8())).blockLast(); } finally { - client.dispose(); + clientRSocket.dispose(); } } } @@ -89,12 +89,10 @@ or you will get a memory leak. Used correctly this will reduce latency and incre ### Example Server setup ```java -RSocketFactory.receive() +RSocketServer.create(new PingHandler()) // Enable Zero Copy - .frameDecoder(PayloadDecoder.ZERO_COPY) - .acceptor(new PingHandler()) - .transport(TcpServerTransport.create(7878)) - .start() + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(TcpServerTransport.create(7878)) .block() .onClose() .block(); @@ -102,12 +100,13 @@ RSocketFactory.receive() ### Example Client setup ```java -Mono client = - RSocketFactory.connect() +RSocket clientRSocket = + RSocketConnector.create() // Enable Zero Copy - .frameDecoder(PayloadDecoder.ZERO_COPY) - .transport(TcpClientTransport.create(7878)) - .start(); + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(TcpClientTransport.create(7878)) + .start() + .block(); ``` ## Bugs and Feedback diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 0d83d06dc..d488d2c08 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -17,6 +17,9 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.core.Resume; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.lease.LeaseStats; import io.rsocket.lease.Leases; @@ -27,7 +30,6 @@ import io.rsocket.resume.ResumeStrategy; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; -import java.lang.reflect.Constructor; import java.time.Duration; import java.util.function.Consumer; import java.util.function.Function; @@ -44,76 +46,171 @@ *
  • {@link ServerRSocketFactory} to start a server. Use {@link #receive()} for a default * instance. * + * + * @deprecated please use {@link RSocketConnector} and {@link RSocketServer}. */ +@Deprecated public final class RSocketFactory { - private static final Constructor clientFactoryConstructor = - getConstructorFor("io.rsocket.core.DefaultClientRSocketFactory"); - - private static final Constructor serverFactoryConstructor = - getConstructorFor("io.rsocket.core.DefaultServerRSocketFactory"); - /** - * Create a {@link ClientRSocketFactory} to connect to a remote RSocket endpoint. A shortcut for - * creating {@link io.rsocket.core.DefaultClientRSocketFactory}. + * Create a {@code ClientRSocketFactory} to connect to a remote RSocket endpoint. Internally + * delegates to {@link RSocketConnector}. * * @return the {@code ClientRSocketFactory} instance */ public static ClientRSocketFactory connect() { - try { - // Avoid explicit dependency and a package cycle - return (ClientRSocketFactory) clientFactoryConstructor.newInstance(); - } catch (Exception ex) { - throw new IllegalStateException("Failed to create ClientRSocketFactory", ex); - } + return new ClientRSocketFactory(); } /** - * Create a {@link ServerRSocketFactory} to accept connections from RSocket clients. A shortcut - * for creating {@link io.rsocket.core.DefaultServerRSocketFactory}. + * Create a {@code ServerRSocketFactory} to accept connections from RSocket clients. Internally + * delegates to {@link RSocketServer}. * * @return the {@code ClientRSocketFactory} instance */ public static ServerRSocketFactory receive() { - try { - // Avoid explicit dependency and a package cycle - return (ServerRSocketFactory) serverFactoryConstructor.newInstance(); - } catch (Exception ex) { - throw new IllegalStateException("Failed to create ServerRSocketFactory", ex); + return new ServerRSocketFactory(); + } + + public interface Start { + Mono start(); + } + + public interface ClientTransportAcceptor { + Start transport(Supplier transport); + + default Start transport(ClientTransport transport) { + return transport(() -> transport); } } + + public interface ServerTransportAcceptor { + + ServerTransport.ConnectionAcceptor toConnectionAcceptor(); + + Start transport(Supplier> transport); + + default Start transport(ServerTransport transport) { + return transport(() -> transport); + } + } + /** Factory to create and configure an RSocket client, and connect to a server. */ - public interface ClientRSocketFactory extends ClientTransportAcceptor { + public static class ClientRSocketFactory implements ClientTransportAcceptor { + private final RSocketConnector connector = RSocketConnector.create(); - ClientRSocketFactory byteBufAllocator(ByteBufAllocator allocator); + private Duration tickPeriod = Duration.ofSeconds(20); + private Duration ackTimeout = Duration.ofSeconds(30); + private int missedAcks = 3; - ClientRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor interceptor); + private Resume resume; - ClientRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor); + public ClientRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { + connector.byteBufAllocator(allocator); + return this; + } - ClientRSocketFactory addResponderPlugin(RSocketInterceptor interceptor); + public ClientRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor interceptor) { + connector.interceptors(registry -> registry.forConnection(interceptor)); + return this; + } - ClientRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor); + /** Deprecated. Use {@link #addRequesterPlugin(RSocketInterceptor)} instead */ + @Deprecated + public ClientRSocketFactory addClientPlugin(RSocketInterceptor interceptor) { + return addRequesterPlugin(interceptor); + } - ClientRSocketFactory keepAlive(Duration tickPeriod, Duration ackTimeout, int missedAcks); + public ClientRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor) { + connector.interceptors(registry -> registry.forRequester(interceptor)); + return this; + } - ClientRSocketFactory keepAliveTickPeriod(Duration tickPeriod); + /** Deprecated. Use {@link #addResponderPlugin(RSocketInterceptor)} instead */ + @Deprecated + public ClientRSocketFactory addServerPlugin(RSocketInterceptor interceptor) { + return addResponderPlugin(interceptor); + } - ClientRSocketFactory keepAliveAckTimeout(Duration ackTimeout); + public ClientRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { + connector.interceptors(registry -> registry.forResponder(interceptor)); + return this; + } - ClientRSocketFactory keepAliveMissedAcks(int missedAcks); + public ClientRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor) { + connector.interceptors(registry -> registry.forSocketAcceptor(interceptor)); + return this; + } - ClientRSocketFactory mimeType(String metadataMimeType, String dataMimeType); + /** + * Deprecated without replacement as Keep-Alive is not optional according to spec + * + * @return this ClientRSocketFactory + */ + @Deprecated + public ClientRSocketFactory keepAlive() { + connector.keepAlive(tickPeriod, ackTimeout.plus(tickPeriod.multipliedBy(missedAcks))); + return this; + } - ClientRSocketFactory dataMimeType(String dataMimeType); + public ClientTransportAcceptor keepAlive( + Duration tickPeriod, Duration ackTimeout, int missedAcks) { + this.tickPeriod = tickPeriod; + this.ackTimeout = ackTimeout; + this.missedAcks = missedAcks; + keepAlive(); + return this; + } - ClientRSocketFactory metadataMimeType(String metadataMimeType); + public ClientRSocketFactory keepAliveTickPeriod(Duration tickPeriod) { + this.tickPeriod = tickPeriod; + keepAlive(); + return this; + } - ClientRSocketFactory lease(Supplier> leasesSupplier); + public ClientRSocketFactory keepAliveAckTimeout(Duration ackTimeout) { + this.ackTimeout = ackTimeout; + keepAlive(); + return this; + } - ClientRSocketFactory lease(); + public ClientRSocketFactory keepAliveMissedAcks(int missedAcks) { + this.missedAcks = missedAcks; + keepAlive(); + return this; + } + + public ClientRSocketFactory mimeType(String metadataMimeType, String dataMimeType) { + connector.metadataMimeType(metadataMimeType); + connector.dataMimeType(dataMimeType); + return this; + } - ClientRSocketFactory singleSubscriberRequester(); + public ClientRSocketFactory dataMimeType(String dataMimeType) { + connector.dataMimeType(dataMimeType); + return this; + } + + public ClientRSocketFactory metadataMimeType(String metadataMimeType) { + connector.metadataMimeType(metadataMimeType); + return this; + } + + public ClientRSocketFactory lease(Supplier> supplier) { + connector.lease(supplier); + return this; + } + + public ClientRSocketFactory lease() { + connector.lease(Leases::new); + return this; + } + + /** @deprecated without a replacement and no longer used. */ + @Deprecated + public ClientRSocketFactory singleSubscriberRequester() { + return this; + } /** * Enables a reconnectable, shared instance of {@code Mono} so every subscriber will @@ -124,7 +221,6 @@ public interface ClientRSocketFactory extends ClientTransportAcceptor { * Mono sharedRSocketMono = * RSocketFactory * .connect() - * .singleSubscriberRequester() * .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1))) * .transport(transport) * .start(); @@ -144,7 +240,6 @@ public interface ClientRSocketFactory extends ClientTransportAcceptor { * Mono sharedRSocketMono = * RSocketFactory * .connect() - * .singleSubscriberRequester() * .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1))) * .transport(transport) * .start(); @@ -175,7 +270,6 @@ public interface ClientRSocketFactory extends ClientTransportAcceptor { * Mono sharedRSocketMono = * RSocketFactory * .connect() - * .singleSubscriberRequester() * .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1))) * .transport(transport) * .start(); @@ -189,108 +283,212 @@ public interface ClientRSocketFactory extends ClientTransportAcceptor { * @param retrySpec a retry factory applied for {@link Mono#retryWhen(Retry)} * @return a shared instance of {@code Mono}. */ - ClientRSocketFactory reconnect(Retry retrySpec); + public ClientRSocketFactory reconnect(Retry retrySpec) { + connector.reconnect(retrySpec); + return this; + } - ClientRSocketFactory resume(); + public ClientRSocketFactory resume() { + resume = resume != null ? resume : new Resume(); + connector.resume(resume); + return this; + } - ClientRSocketFactory resumeToken(Supplier resumeTokenSupplier); + public ClientRSocketFactory resumeToken(Supplier supplier) { + resume(); + resume.token(supplier); + return this; + } - ClientRSocketFactory resumeStore( - Function resumeStoreFactory); + public ClientRSocketFactory resumeStore( + Function storeFactory) { + resume(); + resume.storeFactory(storeFactory); + return this; + } - ClientRSocketFactory resumeSessionDuration(Duration sessionDuration); + public ClientRSocketFactory resumeSessionDuration(Duration sessionDuration) { + resume(); + resume.sessionDuration(sessionDuration); + return this; + } - ClientRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout); + public ClientRSocketFactory resumeStreamTimeout(Duration streamTimeout) { + resume(); + resume.streamTimeout(streamTimeout); + return this; + } - ClientRSocketFactory resumeStrategy(Supplier resumeStrategy); + public ClientRSocketFactory resumeStrategy(Supplier resumeStrategy) { + resume(); + resume.resumeStrategy(resumeStrategy); + return this; + } - ClientRSocketFactory resumeCleanupOnKeepAlive(); + public ClientRSocketFactory resumeCleanupOnKeepAlive() { + resume(); + resume.cleanupStoreOnKeepAlive(); + return this; + } - @Override - Start transport(Supplier transportClient); + public Start transport(Supplier transport) { + return () -> connector.connect(transport); + } - ClientTransportAcceptor acceptor(Function acceptor); + public ClientTransportAcceptor acceptor(Function acceptor) { + return acceptor(() -> acceptor); + } - ClientTransportAcceptor acceptor(Supplier> acceptor); + public ClientTransportAcceptor acceptor(Supplier> acceptorSupplier) { + return acceptor( + (setup, sendingSocket) -> { + acceptorSupplier.get().apply(sendingSocket); + return Mono.empty(); + }); + } - ClientTransportAcceptor acceptor(SocketAcceptor acceptor); + public ClientTransportAcceptor acceptor(SocketAcceptor acceptor) { + connector.acceptor(acceptor); + return this; + } - ClientRSocketFactory fragment(int mtu); + public ClientRSocketFactory fragment(int mtu) { + connector.fragment(mtu); + return this; + } - ClientRSocketFactory errorConsumer(Consumer errorConsumer); + public ClientRSocketFactory errorConsumer(Consumer errorConsumer) { + connector.errorConsumer(errorConsumer); + return this; + } - ClientRSocketFactory setupPayload(Payload payload); + public ClientRSocketFactory setupPayload(Payload payload) { + connector.setupPayload(payload); + return this; + } - ClientRSocketFactory frameDecoder(PayloadDecoder payloadDecoder); + public ClientRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { + connector.payloadDecoder(payloadDecoder); + return this; + } } /** Factory to create, configure, and start an RSocket server. */ - public interface ServerRSocketFactory { - ServerRSocketFactory byteBufAllocator(ByteBufAllocator allocator); - - ServerRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor interceptor); - - ServerRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor); + public static class ServerRSocketFactory implements ServerTransportAcceptor { + private final RSocketServer server = RSocketServer.create(); - ServerRSocketFactory addResponderPlugin(RSocketInterceptor interceptor); + private Resume resume; - ServerRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor); - - ServerTransportAcceptor acceptor(SocketAcceptor acceptor); + public ServerRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { + server.byteBufAllocator(allocator); + return this; + } - ServerRSocketFactory frameDecoder(PayloadDecoder payloadDecoder); + public ServerRSocketFactory addConnectionPlugin(DuplexConnectionInterceptor interceptor) { + server.interceptors(registry -> registry.forConnection(interceptor)); + return this; + } + /** Deprecated. Use {@link #addRequesterPlugin(RSocketInterceptor)} instead */ + @Deprecated + public ServerRSocketFactory addClientPlugin(RSocketInterceptor interceptor) { + return addRequesterPlugin(interceptor); + } - ServerRSocketFactory fragment(int mtu); + public ServerRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor) { + server.interceptors(registry -> registry.forRequester(interceptor)); + return this; + } - ServerRSocketFactory errorConsumer(Consumer errorConsumer); + /** Deprecated. Use {@link #addResponderPlugin(RSocketInterceptor)} instead */ + @Deprecated + public ServerRSocketFactory addServerPlugin(RSocketInterceptor interceptor) { + return addResponderPlugin(interceptor); + } - ServerRSocketFactory lease(Supplier> leasesSupplier); + public ServerRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { + server.interceptors(registry -> registry.forResponder(interceptor)); + return this; + } - ServerRSocketFactory lease(); + public ServerRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor) { + server.interceptors(registry -> registry.forSocketAcceptor(interceptor)); + return this; + } - ServerRSocketFactory singleSubscriberRequester(); + public ServerTransportAcceptor acceptor(SocketAcceptor acceptor) { + return this; + } - ServerRSocketFactory resume(); + public ServerRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { + server.payloadDecoder(payloadDecoder); + return this; + } - ServerRSocketFactory resumeStore( - Function resumeStoreFactory); + public ServerRSocketFactory fragment(int mtu) { + server.fragment(mtu); + return this; + } - ServerRSocketFactory resumeSessionDuration(Duration sessionDuration); + public ServerRSocketFactory errorConsumer(Consumer errorConsumer) { + server.errorConsumer(errorConsumer); + return this; + } - ServerRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout); + public ServerRSocketFactory lease(Supplier> supplier) { + server.lease(supplier); + return this; + } - ServerRSocketFactory resumeCleanupOnKeepAlive(); - } + public ServerRSocketFactory lease() { + server.lease(Leases::new); + return this; + } - public interface ClientTransportAcceptor { - Start transport(Supplier transport); + /** @deprecated without a replacement and no longer used. */ + @Deprecated + public ServerRSocketFactory singleSubscriberRequester() { + return this; + } - default Start transport(ClientTransport transport) { - return transport(() -> transport); + public ServerRSocketFactory resume() { + resume = resume != null ? resume : new Resume(); + server.resume(resume); + return this; } - } - public interface ServerTransportAcceptor { + public ServerRSocketFactory resumeStore( + Function storeFactory) { + resume(); + resume.storeFactory(storeFactory); + return this; + } - ServerTransport.ConnectionAcceptor toConnectionAcceptor(); + public ServerRSocketFactory resumeSessionDuration(Duration sessionDuration) { + resume(); + resume.sessionDuration(sessionDuration); + return this; + } - Start transport(Supplier> transport); + public ServerRSocketFactory resumeStreamTimeout(Duration streamTimeout) { + resume(); + resume.streamTimeout(streamTimeout); + return this; + } - default Start transport(ServerTransport transport) { - return transport(() -> transport); + public ServerRSocketFactory resumeCleanupOnKeepAlive() { + resume(); + resume.cleanupStoreOnKeepAlive(); + return this; } - } - public interface Start { - Mono start(); - } + @Override + public ServerTransport.ConnectionAcceptor toConnectionAcceptor() { + return server.asConnectionAcceptor(); + } - private static Constructor getConstructorFor(String className) { - try { - Class clazz = Class.forName(className); - return clazz.getDeclaredConstructor(); - } catch (Throwable ex) { - throw new IllegalStateException("No " + className); + @Override + public Start transport(Supplier> transport) { + return () -> server.bind(transport.get()); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java deleted file mode 100644 index 2ecde3663..000000000 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultClientRSocketFactory.java +++ /dev/null @@ -1,436 +0,0 @@ -/* - * 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 - * - * https://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.core; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.rsocket.AbstractRSocket; -import io.rsocket.ConnectionSetupPayload; -import io.rsocket.DuplexConnection; -import io.rsocket.Payload; -import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; -import io.rsocket.SocketAcceptor; -import io.rsocket.frame.ResumeFrameFlyweight; -import io.rsocket.frame.SetupFrameFlyweight; -import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.ClientServerInputMultiplexer; -import io.rsocket.keepalive.KeepAliveHandler; -import io.rsocket.lease.LeaseStats; -import io.rsocket.lease.Leases; -import io.rsocket.lease.RequesterLeaseHandler; -import io.rsocket.lease.ResponderLeaseHandler; -import io.rsocket.plugins.DuplexConnectionInterceptor; -import io.rsocket.plugins.PluginRegistry; -import io.rsocket.plugins.Plugins; -import io.rsocket.plugins.RSocketInterceptor; -import io.rsocket.plugins.SocketAcceptorInterceptor; -import io.rsocket.resume.ClientRSocketSession; -import io.rsocket.resume.ExponentialBackoffResumeStrategy; -import io.rsocket.resume.InMemoryResumableFramesStore; -import io.rsocket.resume.ResumableFramesStore; -import io.rsocket.resume.ResumeStrategy; -import io.rsocket.transport.ClientTransport; -import io.rsocket.util.EmptyPayload; -import io.rsocket.util.MultiSubscriberRSocket; -import java.time.Duration; -import java.util.Objects; -import java.util.function.BiConsumer; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import reactor.core.Disposable; -import reactor.core.publisher.Mono; -import reactor.util.retry.Retry; - -/** - * Default implementation of {@link RSocketFactory.ClientRSocketFactory} that can be instantiated - * directly or through the shortcut {@link RSocketFactory#connect()}. - */ -public class DefaultClientRSocketFactory implements RSocketFactory.ClientRSocketFactory { - private static final String CLIENT_TAG = "client"; - - private static final BiConsumer INVALIDATE_FUNCTION = - (r, i) -> r.onClose().subscribe(null, null, i::invalidate); - - private SocketAcceptor acceptor = (setup, sendingSocket) -> Mono.just(new AbstractRSocket() {}); - - private Consumer errorConsumer = Throwable::printStackTrace; - private int mtu = 0; - private PluginRegistry plugins = new PluginRegistry(Plugins.defaultPlugins()); - - private Payload setupPayload = EmptyPayload.INSTANCE; - private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; - - private Duration tickPeriod = Duration.ofSeconds(20); - private Duration ackTimeout = Duration.ofSeconds(30); - private int missedAcks = 3; - - private String metadataMimeType = "application/binary"; - private String dataMimeType = "application/binary"; - - private boolean resumeEnabled; - private boolean resumeCleanupStoreOnKeepAlive; - private Supplier resumeTokenSupplier = ResumeFrameFlyweight::generateResumeToken; - private Function resumeStoreFactory = - token -> new InMemoryResumableFramesStore(CLIENT_TAG, 100_000); - private Duration resumeSessionDuration = Duration.ofMinutes(2); - private Duration resumeStreamTimeout = Duration.ofSeconds(10); - private Supplier resumeStrategySupplier = - () -> new ExponentialBackoffResumeStrategy(Duration.ofSeconds(1), Duration.ofSeconds(16), 2); - - private boolean multiSubscriberRequester = true; - private boolean leaseEnabled; - private Supplier> leasesSupplier = Leases::new; - private boolean reconnectEnabled; - private Retry retrySpec; - - private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; - - @Override - public RSocketFactory.ClientRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { - Objects.requireNonNull(allocator); - this.allocator = allocator; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory addConnectionPlugin( - DuplexConnectionInterceptor interceptor) { - plugins.addConnectionPlugin(interceptor); - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor) { - plugins.addRequesterPlugin(interceptor); - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { - plugins.addResponderPlugin(interceptor); - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory addSocketAcceptorPlugin( - SocketAcceptorInterceptor interceptor) { - plugins.addSocketAcceptorPlugin(interceptor); - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory keepAlive( - Duration tickPeriod, Duration ackTimeout, int missedAcks) { - this.tickPeriod = tickPeriod; - this.ackTimeout = ackTimeout; - this.missedAcks = missedAcks; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory keepAliveTickPeriod(Duration tickPeriod) { - this.tickPeriod = tickPeriod; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory keepAliveAckTimeout(Duration ackTimeout) { - this.ackTimeout = ackTimeout; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory keepAliveMissedAcks(int missedAcks) { - this.missedAcks = missedAcks; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory mimeType( - String metadataMimeType, String dataMimeType) { - this.dataMimeType = dataMimeType; - this.metadataMimeType = metadataMimeType; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory dataMimeType(String dataMimeType) { - this.dataMimeType = dataMimeType; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory metadataMimeType(String metadataMimeType) { - this.metadataMimeType = metadataMimeType; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory lease( - Supplier> leasesSupplier) { - this.leaseEnabled = true; - this.leasesSupplier = Objects.requireNonNull(leasesSupplier); - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory lease() { - this.leaseEnabled = true; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory singleSubscriberRequester() { - this.multiSubscriberRequester = false; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory reconnect(Retry retrySpec) { - this.retrySpec = Objects.requireNonNull(retrySpec); - this.reconnectEnabled = true; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory resume() { - this.resumeEnabled = true; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory resumeToken(Supplier resumeTokenSupplier) { - this.resumeTokenSupplier = Objects.requireNonNull(resumeTokenSupplier); - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory resumeStore( - Function resumeStoreFactory) { - this.resumeStoreFactory = resumeStoreFactory; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory resumeSessionDuration(Duration sessionDuration) { - this.resumeSessionDuration = Objects.requireNonNull(sessionDuration); - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout) { - this.resumeStreamTimeout = Objects.requireNonNull(resumeStreamTimeout); - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory resumeStrategy( - Supplier resumeStrategy) { - this.resumeStrategySupplier = Objects.requireNonNull(resumeStrategy); - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory resumeCleanupOnKeepAlive() { - resumeCleanupStoreOnKeepAlive = true; - return this; - } - - @Override - public RSocketFactory.Start transport(Supplier transportClient) { - return new StartClient(transportClient); - } - - @Override - public RSocketFactory.ClientTransportAcceptor acceptor(Function acceptor) { - return acceptor(() -> acceptor); - } - - @Override - public RSocketFactory.ClientTransportAcceptor acceptor( - Supplier> acceptor) { - return acceptor((setup, sendingSocket) -> Mono.just(acceptor.get().apply(sendingSocket))); - } - - @Override - public RSocketFactory.ClientTransportAcceptor acceptor(SocketAcceptor acceptor) { - this.acceptor = acceptor; - return StartClient::new; - } - - @Override - public RSocketFactory.ClientRSocketFactory fragment(int mtu) { - this.mtu = mtu; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory errorConsumer(Consumer errorConsumer) { - this.errorConsumer = errorConsumer; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory setupPayload(Payload payload) { - this.setupPayload = payload; - return this; - } - - @Override - public RSocketFactory.ClientRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { - this.payloadDecoder = payloadDecoder; - return this; - } - - private class StartClient implements RSocketFactory.Start { - private final Supplier transportClient; - - StartClient(Supplier transportClient) { - this.transportClient = transportClient; - } - - @Override - public Mono start() { - return newConnection() - .flatMap( - connection -> { - ByteBuf resumeToken; - KeepAliveHandler keepAliveHandler; - DuplexConnection wrappedConnection; - - if (resumeEnabled) { - resumeToken = resumeTokenSupplier.get(); - ClientRSocketSession session = - new ClientRSocketSession( - connection, - allocator, - resumeSessionDuration, - resumeStrategySupplier, - resumeStoreFactory.apply(resumeToken), - resumeStreamTimeout, - resumeCleanupStoreOnKeepAlive) - .continueWith(newConnection()) - .resumeToken(resumeToken); - keepAliveHandler = - new KeepAliveHandler.ResumableKeepAliveHandler(session.resumableConnection()); - wrappedConnection = session.resumableConnection(); - } else { - resumeToken = Unpooled.EMPTY_BUFFER; - keepAliveHandler = new KeepAliveHandler.DefaultKeepAliveHandler(connection); - wrappedConnection = connection; - } - - ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(wrappedConnection, plugins, true); - - boolean isLeaseEnabled = leaseEnabled; - Leases leases = leasesSupplier.get(); - RequesterLeaseHandler requesterLeaseHandler = - isLeaseEnabled - ? new RequesterLeaseHandler.Impl(CLIENT_TAG, leases.receiver()) - : RequesterLeaseHandler.None; - - RSocket rSocketRequester = - new RSocketRequester( - allocator, - multiplexer.asClientConnection(), - payloadDecoder, - errorConsumer, - StreamIdSupplier.clientSupplier(), - mtu, - keepAliveTickPeriod(), - keepAliveTimeout(), - keepAliveHandler, - requesterLeaseHandler); - - if (multiSubscriberRequester) { - rSocketRequester = new MultiSubscriberRSocket(rSocketRequester); - } - - RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); - - ByteBuf setupFrame = - SetupFrameFlyweight.encode( - allocator, - isLeaseEnabled, - keepAliveTickPeriod(), - keepAliveTimeout(), - resumeToken, - metadataMimeType, - dataMimeType, - setupPayload); - - ConnectionSetupPayload setup = new DefaultConnectionSetupPayload(setupFrame); - - return plugins - .applySocketAcceptorInterceptor(acceptor) - .accept(setup, wrappedRSocketRequester) - .flatMap( - rSocketHandler -> { - RSocket wrappedRSocketHandler = plugins.applyResponder(rSocketHandler); - - ResponderLeaseHandler responderLeaseHandler = - isLeaseEnabled - ? new ResponderLeaseHandler.Impl<>( - CLIENT_TAG, - allocator, - leases.sender(), - errorConsumer, - leases.stats()) - : ResponderLeaseHandler.None; - - RSocket rSocketResponder = - new RSocketResponder( - allocator, - multiplexer.asServerConnection(), - wrappedRSocketHandler, - payloadDecoder, - errorConsumer, - responderLeaseHandler, - mtu); - - return wrappedConnection - .sendOne(setupFrame) - .thenReturn(wrappedRSocketRequester); - }); - }) - .as( - source -> { - if (reconnectEnabled) { - return new ReconnectMono<>( - source.retryWhen(retrySpec), Disposable::dispose, INVALIDATE_FUNCTION); - } else { - return source; - } - }); - } - - private int keepAliveTickPeriod() { - return (int) tickPeriod.toMillis(); - } - - private int keepAliveTimeout() { - return (int) (ackTimeout.toMillis() + tickPeriod.toMillis() * missedAcks); - } - - private Mono newConnection() { - return Mono.fromSupplier(transportClient).flatMap(t -> t.connect(mtu)); - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java deleted file mode 100644 index 73d3513da..000000000 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultServerRSocketFactory.java +++ /dev/null @@ -1,377 +0,0 @@ -/* - * 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 - * - * https://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.core; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.rsocket.Closeable; -import io.rsocket.ConnectionSetupPayload; -import io.rsocket.DuplexConnection; -import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; -import io.rsocket.SocketAcceptor; -import io.rsocket.exceptions.InvalidSetupException; -import io.rsocket.exceptions.RejectedSetupException; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.SetupFrameFlyweight; -import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.internal.ClientServerInputMultiplexer; -import io.rsocket.lease.Leases; -import io.rsocket.lease.RequesterLeaseHandler; -import io.rsocket.lease.ResponderLeaseHandler; -import io.rsocket.plugins.DuplexConnectionInterceptor; -import io.rsocket.plugins.PluginRegistry; -import io.rsocket.plugins.Plugins; -import io.rsocket.plugins.RSocketInterceptor; -import io.rsocket.plugins.SocketAcceptorInterceptor; -import io.rsocket.resume.InMemoryResumableFramesStore; -import io.rsocket.resume.ResumableFramesStore; -import io.rsocket.resume.SessionManager; -import io.rsocket.transport.ServerTransport; -import io.rsocket.util.MultiSubscriberRSocket; -import java.time.Duration; -import java.util.Objects; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; -import reactor.core.publisher.Mono; - -/** - * Default implementation of {@link RSocketFactory.ServerRSocketFactory} that can be instantiated - * directly or through the shortcut {@link RSocketFactory#receive()}. - */ -public class DefaultServerRSocketFactory implements RSocketFactory.ServerRSocketFactory { - private static final String SERVER_TAG = "server"; - - private SocketAcceptor acceptor; - private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; - private Consumer errorConsumer = Throwable::printStackTrace; - private int mtu = 0; - private PluginRegistry plugins = new PluginRegistry(Plugins.defaultPlugins()); - - private boolean resumeSupported; - private Duration resumeSessionDuration = Duration.ofSeconds(120); - private Duration resumeStreamTimeout = Duration.ofSeconds(10); - private Function resumeStoreFactory = - token -> new InMemoryResumableFramesStore(SERVER_TAG, 100_000); - - private boolean multiSubscriberRequester = true; - private boolean leaseEnabled; - private Supplier> leasesSupplier = Leases::new; - - private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; - private boolean resumeCleanupStoreOnKeepAlive; - - @Override - public RSocketFactory.ServerRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { - Objects.requireNonNull(allocator); - this.allocator = allocator; - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory addConnectionPlugin( - DuplexConnectionInterceptor interceptor) { - plugins.addConnectionPlugin(interceptor); - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory addRequesterPlugin(RSocketInterceptor interceptor) { - plugins.addRequesterPlugin(interceptor); - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory addResponderPlugin(RSocketInterceptor interceptor) { - plugins.addResponderPlugin(interceptor); - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory addSocketAcceptorPlugin( - SocketAcceptorInterceptor interceptor) { - plugins.addSocketAcceptorPlugin(interceptor); - return this; - } - - @Override - public RSocketFactory.ServerTransportAcceptor acceptor(SocketAcceptor acceptor) { - this.acceptor = acceptor; - return new ServerStart<>(); - } - - @Override - public RSocketFactory.ServerRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { - this.payloadDecoder = payloadDecoder; - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory fragment(int mtu) { - this.mtu = mtu; - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory errorConsumer(Consumer errorConsumer) { - this.errorConsumer = errorConsumer; - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory lease(Supplier> leasesSupplier) { - this.leaseEnabled = true; - this.leasesSupplier = Objects.requireNonNull(leasesSupplier); - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory lease() { - this.leaseEnabled = true; - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory singleSubscriberRequester() { - this.multiSubscriberRequester = false; - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory resume() { - this.resumeSupported = true; - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory resumeStore( - Function resumeStoreFactory) { - this.resumeStoreFactory = resumeStoreFactory; - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory resumeSessionDuration(Duration sessionDuration) { - this.resumeSessionDuration = Objects.requireNonNull(sessionDuration); - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory resumeStreamTimeout(Duration resumeStreamTimeout) { - this.resumeStreamTimeout = Objects.requireNonNull(resumeStreamTimeout); - return this; - } - - @Override - public RSocketFactory.ServerRSocketFactory resumeCleanupOnKeepAlive() { - resumeCleanupStoreOnKeepAlive = true; - return this; - } - - private class ServerStart - implements RSocketFactory.Start, RSocketFactory.ServerTransportAcceptor { - private Supplier> transportServer; - - @Override - public ServerTransport.ConnectionAcceptor toConnectionAcceptor() { - return new ServerTransport.ConnectionAcceptor() { - private final ServerSetup serverSetup = serverSetup(); - - @Override - public Mono apply(DuplexConnection connection) { - return acceptor(serverSetup, connection); - } - }; - } - - @Override - @SuppressWarnings("unchecked") - public RSocketFactory.Start transport( - Supplier> transport) { - this.transportServer = (Supplier) transport; - return (RSocketFactory.Start) this::start; - } - - private Mono acceptor(ServerSetup serverSetup, DuplexConnection connection) { - ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(connection, plugins, false); - - return multiplexer - .asSetupConnection() - .receive() - .next() - .flatMap(startFrame -> accept(serverSetup, startFrame, multiplexer)); - } - - private Mono acceptResume( - ServerSetup serverSetup, ByteBuf resumeFrame, ClientServerInputMultiplexer multiplexer) { - return serverSetup.acceptRSocketResume(resumeFrame, multiplexer); - } - - private Mono accept( - ServerSetup serverSetup, ByteBuf startFrame, ClientServerInputMultiplexer multiplexer) { - switch (FrameHeaderFlyweight.frameType(startFrame)) { - case SETUP: - return acceptSetup(serverSetup, startFrame, multiplexer); - case RESUME: - return acceptResume(serverSetup, startFrame, multiplexer); - default: - return serverSetup - .sendError( - multiplexer, - new InvalidSetupException( - "invalid setup frame: " + FrameHeaderFlyweight.frameType(startFrame))) - .doFinally( - signalType -> { - startFrame.release(); - multiplexer.dispose(); - }); - } - } - - private Mono acceptSetup( - ServerSetup serverSetup, ByteBuf setupFrame, ClientServerInputMultiplexer multiplexer) { - - if (!SetupFrameFlyweight.isSupportedVersion(setupFrame)) { - return serverSetup - .sendError( - multiplexer, - new InvalidSetupException( - "Unsupported version: " + SetupFrameFlyweight.humanReadableVersion(setupFrame))) - .doFinally( - signalType -> { - setupFrame.release(); - multiplexer.dispose(); - }); - } - - boolean isLeaseEnabled = leaseEnabled; - - if (SetupFrameFlyweight.honorLease(setupFrame) && !isLeaseEnabled) { - return serverSetup - .sendError(multiplexer, new InvalidSetupException("lease is not supported")) - .doFinally( - signalType -> { - setupFrame.release(); - multiplexer.dispose(); - }); - } - - return serverSetup.acceptRSocketSetup( - setupFrame, - multiplexer, - (keepAliveHandler, wrappedMultiplexer) -> { - ConnectionSetupPayload setupPayload = new DefaultConnectionSetupPayload(setupFrame); - - Leases leases = leasesSupplier.get(); - RequesterLeaseHandler requesterLeaseHandler = - isLeaseEnabled - ? new RequesterLeaseHandler.Impl(SERVER_TAG, leases.receiver()) - : RequesterLeaseHandler.None; - - RSocket rSocketRequester = - new RSocketRequester( - allocator, - wrappedMultiplexer.asServerConnection(), - payloadDecoder, - errorConsumer, - StreamIdSupplier.serverSupplier(), - mtu, - setupPayload.keepAliveInterval(), - setupPayload.keepAliveMaxLifetime(), - keepAliveHandler, - requesterLeaseHandler); - - if (multiSubscriberRequester) { - rSocketRequester = new MultiSubscriberRSocket(rSocketRequester); - } - RSocket wrappedRSocketRequester = plugins.applyRequester(rSocketRequester); - - return plugins - .applySocketAcceptorInterceptor(acceptor) - .accept(setupPayload, wrappedRSocketRequester) - .onErrorResume( - err -> - serverSetup - .sendError(multiplexer, rejectedSetupError(err)) - .then(Mono.error(err))) - .doOnNext( - rSocketHandler -> { - RSocket wrappedRSocketHandler = plugins.applyResponder(rSocketHandler); - - ResponderLeaseHandler responderLeaseHandler = - isLeaseEnabled - ? new ResponderLeaseHandler.Impl<>( - SERVER_TAG, - allocator, - leases.sender(), - errorConsumer, - leases.stats()) - : ResponderLeaseHandler.None; - - RSocket rSocketResponder = - new RSocketResponder( - allocator, - wrappedMultiplexer.asClientConnection(), - wrappedRSocketHandler, - payloadDecoder, - errorConsumer, - responderLeaseHandler, - mtu); - }) - .doFinally(signalType -> setupPayload.release()) - .then(); - }); - } - - @Override - public Mono start() { - return Mono.defer( - new Supplier>() { - - ServerSetup serverSetup = serverSetup(); - - @Override - public Mono get() { - return Mono.fromSupplier(transportServer) - .flatMap( - transport -> - transport.start( - duplexConnection -> acceptor(serverSetup, duplexConnection), mtu)) - .doOnNext(c -> c.onClose().doFinally(v -> serverSetup.dispose()).subscribe()); - } - }); - } - - private ServerSetup serverSetup() { - return resumeSupported - ? new ServerSetup.ResumableServerSetup( - allocator, - new SessionManager(), - resumeSessionDuration, - resumeStreamTimeout, - resumeStoreFactory, - resumeCleanupStoreOnKeepAlive) - : new ServerSetup.DefaultServerSetup(allocator); - } - - private Exception rejectedSetupError(Throwable err) { - String msg = err.getMessage(); - return new RejectedSetupException(msg == null ? "rejected by server acceptor" : msg); - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java new file mode 100644 index 000000000..92c5220c0 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -0,0 +1,375 @@ +/* + * 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 + * + * https://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.core; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.AbstractRSocket; +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.DuplexConnection; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.ClientServerInputMultiplexer; +import io.rsocket.keepalive.KeepAliveHandler; +import io.rsocket.lease.LeaseStats; +import io.rsocket.lease.Leases; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.plugins.InitializingInterceptorRegistry; +import io.rsocket.plugins.InterceptorRegistry; +import io.rsocket.resume.ClientRSocketSession; +import io.rsocket.transport.ClientTransport; +import io.rsocket.util.EmptyPayload; +import java.time.Duration; +import java.util.Objects; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Supplier; +import reactor.core.Disposable; +import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; + +public class RSocketConnector { + private static final String CLIENT_TAG = "client"; + private static final int MIN_MTU_SIZE = 64; + + private static final BiConsumer INVALIDATE_FUNCTION = + (r, i) -> r.onClose().subscribe(null, null, i::invalidate); + + private Payload setupPayload = EmptyPayload.INSTANCE; + private String metadataMimeType = "application/binary"; + private String dataMimeType = "application/binary"; + + private SocketAcceptor acceptor = (setup, sendingSocket) -> Mono.just(new AbstractRSocket() {}); + private InitializingInterceptorRegistry interceptors = new InitializingInterceptorRegistry(); + + private Duration keepAliveInterval = Duration.ofSeconds(20); + private Duration keepAliveMaxLifeTime = Duration.ofSeconds(90); + + private Retry retrySpec; + private Resume resume; + private Supplier> leasesSupplier; + + private int mtu = 0; + private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; + + private Consumer errorConsumer = Throwable::printStackTrace; + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + + private RSocketConnector() {} + + public static RSocketConnector create() { + return new RSocketConnector(); + } + + public static Mono connectWith(ClientTransport transport) { + return RSocketConnector.create().connect(() -> transport); + } + + public RSocketConnector setupPayload(Payload payload) { + this.setupPayload = payload; + return this; + } + + public RSocketConnector dataMimeType(String dataMimeType) { + this.dataMimeType = dataMimeType; + return this; + } + + public RSocketConnector metadataMimeType(String metadataMimeType) { + this.metadataMimeType = metadataMimeType; + return this; + } + + public RSocketConnector interceptors(Consumer consumer) { + consumer.accept(this.interceptors); + return this; + } + + public RSocketConnector acceptor(SocketAcceptor acceptor) { + this.acceptor = acceptor; + return this; + } + + /** + * Set the time {@code interval} between KEEPALIVE frames sent by this client, and the {@code + * maxLifeTime} that this client will allow between KEEPALIVE frames from the server before + * assuming it is dead. + * + *

    Note that reasonable values for the time interval may vary significantly. For + * server-to-server connections the spec suggests 500ms, while for for mobile-to-server + * connections it suggests 30+ seconds. In addition {@code maxLifeTime} should allow plenty of + * room for multiple missed ticks from the server. + * + *

    By default {@code interval} is set to 20 seconds and {@code maxLifeTime} to 90 seconds. + * + * @param interval the time between KEEPALIVE frames sent, must be > 0. + * @param maxLifeTime the max time allowed between KEEPALIVE frames received, must be > 0. + */ + public RSocketConnector keepAlive(Duration interval, Duration maxLifeTime) { + if (!interval.negated().isNegative()) { + throw new IllegalArgumentException("`interval` for keepAlive must be > 0"); + } + if (!maxLifeTime.negated().isNegative()) { + throw new IllegalArgumentException("`maxLifeTime` for keepAlive must be > 0"); + } + this.keepAliveInterval = interval; + this.keepAliveMaxLifeTime = maxLifeTime; + return this; + } + + /** + * Enables a reconnectable, shared instance of {@code Mono} so every subscriber will + * observe the same RSocket instance up on connection establishment.
    + * For example: + * + *

    {@code
    +   * Mono sharedRSocketMono =
    +   *   RSocketConnector.create()
    +   *           .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
    +   *           .connect(transport);
    +   *
    +   *  RSocket r1 = sharedRSocketMono.block();
    +   *  RSocket r2 = sharedRSocketMono.block();
    +   *
    +   *  assert r1 == r2;
    +   *
    +   * }
    + * + * Apart of the shared behavior, if the connection is lost, the same {@code Mono} + * instance will transparently re-establish the connection for subsequent subscribers.
    + * For example: + * + *
    {@code
    +   * Mono sharedRSocketMono =
    +   *   RSocketConnector.create()
    +   *           .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
    +   *           .connect(transport);
    +   *
    +   *  RSocket r1 = sharedRSocketMono.block();
    +   *  RSocket r2 = sharedRSocketMono.block();
    +   *
    +   *  assert r1 == r2;
    +   *
    +   *  r1.dispose()
    +   *
    +   *  assert r2.isDisposed()
    +   *
    +   *  RSocket r3 = sharedRSocketMono.block();
    +   *  RSocket r4 = sharedRSocketMono.block();
    +   *
    +   *  assert r1 != r3;
    +   *  assert r4 == r3;
    +   *
    +   * }
    + * + * Note, having reconnect() enabled does not eliminate the need to accompany each + * individual request with the corresponding retry logic.
    + * For example: + * + *
    {@code
    +   * Mono sharedRSocketMono =
    +   *   RSocketConnector.create()
    +   *           .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
    +   *           .connect(transport);
    +   *
    +   *  sharedRSocket.flatMap(rSocket -> rSocket.requestResponse(...))
    +   *               .retryWhen(ownRetry)
    +   *               .subscribe()
    +   *
    +   * }
    + * + * @param retrySpec a retry factory applied for {@link Mono#retryWhen(Retry)} + * @return a shared instance of {@code Mono}. + */ + public RSocketConnector reconnect(Retry retrySpec) { + this.retrySpec = Objects.requireNonNull(retrySpec); + return this; + } + + public RSocketConnector resume(Resume resume) { + this.resume = resume; + return this; + } + + public RSocketConnector lease(Supplier> supplier) { + this.leasesSupplier = supplier; + return this; + } + + public RSocketConnector fragment(int mtu) { + if (mtu > 0 && mtu < MIN_MTU_SIZE || mtu < 0) { + String msg = + String.format("smallest allowed mtu size is %d bytes, provided: %d", MIN_MTU_SIZE, mtu); + throw new IllegalArgumentException(msg); + } + this.mtu = mtu; + return this; + } + + public RSocketConnector payloadDecoder(PayloadDecoder payloadDecoder) { + Objects.requireNonNull(payloadDecoder); + this.payloadDecoder = payloadDecoder; + return this; + } + + /** + * @deprecated this is deprecated with no replacement and will be removed after {@link + * io.rsocket.RSocketFactory} is removed. + */ + @Deprecated + public RSocketConnector errorConsumer(Consumer errorConsumer) { + Objects.requireNonNull(errorConsumer); + this.errorConsumer = errorConsumer; + return this; + } + + /** + * @deprecated this is deprecated with no replacement and will be removed after {@link + * io.rsocket.RSocketFactory} is removed. + */ + public RSocketConnector byteBufAllocator(ByteBufAllocator allocator) { + Objects.requireNonNull(allocator); + this.allocator = allocator; + return this; + } + + public Mono connect(ClientTransport transport) { + return connect(() -> transport); + } + + public Mono connect(Supplier transportSupplier) { + Mono connectionMono = + Mono.fromSupplier(transportSupplier).flatMap(t -> t.connect(mtu)); + return connectionMono + .flatMap( + connection -> { + ByteBuf resumeToken; + KeepAliveHandler keepAliveHandler; + DuplexConnection wrappedConnection; + + if (resume != null) { + ClientRSocketSession session = createSession(connection, connectionMono); + resumeToken = session.token(); + keepAliveHandler = + new KeepAliveHandler.ResumableKeepAliveHandler(session.resumableConnection()); + wrappedConnection = session.resumableConnection(); + } else { + resumeToken = Unpooled.EMPTY_BUFFER; + keepAliveHandler = new KeepAliveHandler.DefaultKeepAliveHandler(connection); + wrappedConnection = connection; + } + + ClientServerInputMultiplexer multiplexer = + new ClientServerInputMultiplexer(wrappedConnection, interceptors, true); + + boolean leaseEnabled = leasesSupplier != null; + Leases leases = leaseEnabled ? leasesSupplier.get() : null; + RequesterLeaseHandler requesterLeaseHandler = + leaseEnabled + ? new RequesterLeaseHandler.Impl(CLIENT_TAG, leases.receiver()) + : RequesterLeaseHandler.None; + + RSocket rSocketRequester = + new RSocketRequester( + allocator, + multiplexer.asClientConnection(), + payloadDecoder, + errorConsumer, + StreamIdSupplier.clientSupplier(), + mtu, + (int) keepAliveInterval.toMillis(), + (int) keepAliveMaxLifeTime.toMillis(), + keepAliveHandler, + requesterLeaseHandler); + + RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); + + ByteBuf setupFrame = + SetupFrameFlyweight.encode( + allocator, + leaseEnabled, + (int) keepAliveInterval.toMillis(), + (int) keepAliveMaxLifeTime.toMillis(), + resumeToken, + metadataMimeType, + dataMimeType, + setupPayload); + + ConnectionSetupPayload setup = new DefaultConnectionSetupPayload(setupFrame); + + return interceptors + .initSocketAcceptor(acceptor) + .accept(setup, wrappedRSocketRequester) + .flatMap( + rSocketHandler -> { + RSocket wrappedRSocketHandler = interceptors.initResponder(rSocketHandler); + + ResponderLeaseHandler responderLeaseHandler = + leaseEnabled + ? new ResponderLeaseHandler.Impl<>( + CLIENT_TAG, + allocator, + leases.sender(), + errorConsumer, + leases.stats()) + : ResponderLeaseHandler.None; + + RSocket rSocketResponder = + new RSocketResponder( + allocator, + multiplexer.asServerConnection(), + wrappedRSocketHandler, + payloadDecoder, + errorConsumer, + responderLeaseHandler, + mtu); + + return wrappedConnection + .sendOne(setupFrame) + .thenReturn(wrappedRSocketRequester); + }); + }) + .as( + source -> { + if (retrySpec != null) { + return new ReconnectMono<>( + source.retryWhen(retrySpec), Disposable::dispose, INVALIDATE_FUNCTION); + } else { + return source; + } + }); + } + + private ClientRSocketSession createSession( + DuplexConnection connection, Mono connectionMono) { + ByteBuf resumeToken = resume.getTokenSupplier().get(); + ClientRSocketSession session = + new ClientRSocketSession( + connection, + allocator, + resume.getSessionDuration(), + resume.getResumeStrategySupplier(), + resume.getStoreFactory(CLIENT_TAG).apply(resumeToken), + resume.getStreamTimeout(), + resume.isCleanupStoreOnKeepAlive()); + return session.continueWith(connectionMono).resumeToken(resumeToken); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java new file mode 100644 index 000000000..113b4283f --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -0,0 +1,299 @@ +/* + * 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 + * + * https://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.core; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.AbstractRSocket; +import io.rsocket.Closeable; +import io.rsocket.ConnectionSetupPayload; +import io.rsocket.DuplexConnection; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; +import io.rsocket.exceptions.InvalidSetupException; +import io.rsocket.exceptions.RejectedSetupException; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.ClientServerInputMultiplexer; +import io.rsocket.lease.Leases; +import io.rsocket.lease.RequesterLeaseHandler; +import io.rsocket.lease.ResponderLeaseHandler; +import io.rsocket.plugins.InitializingInterceptorRegistry; +import io.rsocket.plugins.InterceptorRegistry; +import io.rsocket.resume.SessionManager; +import io.rsocket.transport.ServerTransport; +import java.util.Objects; +import java.util.function.Consumer; +import java.util.function.Supplier; +import reactor.core.publisher.Mono; + +public final class RSocketServer { + private static final String SERVER_TAG = "server"; + private static final int MIN_MTU_SIZE = 64; + + private SocketAcceptor acceptor = (setup, sendingSocket) -> Mono.just(new AbstractRSocket() {}); + private InitializingInterceptorRegistry interceptors = new InitializingInterceptorRegistry(); + private int mtu = 0; + + private Resume resume; + private Supplier> leasesSupplier = null; + + private Consumer errorConsumer = Throwable::printStackTrace; + private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + + private RSocketServer() {} + + public static RSocketServer create() { + return new RSocketServer(); + } + + public static RSocketServer create(SocketAcceptor acceptor) { + return RSocketServer.create().acceptor(acceptor); + } + + public RSocketServer acceptor(SocketAcceptor acceptor) { + Objects.requireNonNull(acceptor); + this.acceptor = acceptor; + return this; + } + + public RSocketServer interceptors(Consumer consumer) { + consumer.accept(this.interceptors); + return this; + } + + public RSocketServer fragment(int mtu) { + if (mtu > 0 && mtu < MIN_MTU_SIZE || mtu < 0) { + String msg = + String.format("smallest allowed mtu size is %d bytes, provided: %d", MIN_MTU_SIZE, mtu); + throw new IllegalArgumentException(msg); + } + this.mtu = mtu; + return this; + } + + public RSocketServer resume(Resume resume) { + this.resume = resume; + return this; + } + + public RSocketServer lease(Supplier> supplier) { + this.leasesSupplier = supplier; + return this; + } + + public RSocketServer payloadDecoder(PayloadDecoder payloadDecoder) { + Objects.requireNonNull(payloadDecoder); + this.payloadDecoder = payloadDecoder; + return this; + } + + /** + * @deprecated this is deprecated with no replacement and will be removed after {@link + * io.rsocket.RSocketFactory} is removed. + */ + @Deprecated + public RSocketServer errorConsumer(Consumer errorConsumer) { + this.errorConsumer = errorConsumer; + return this; + } + + /** + * @deprecated this is deprecated with no replacement and will be removed after {@link + * io.rsocket.RSocketFactory} is removed. + */ + @Deprecated + public RSocketServer byteBufAllocator(ByteBufAllocator allocator) { + Objects.requireNonNull(allocator); + this.allocator = allocator; + return this; + } + + public ServerTransport.ConnectionAcceptor asConnectionAcceptor() { + return new ServerTransport.ConnectionAcceptor() { + private final ServerSetup serverSetup = serverSetup(); + + @Override + public Mono apply(DuplexConnection connection) { + return acceptor(serverSetup, connection); + } + }; + } + + public Mono bind(ServerTransport transport) { + return Mono.defer( + new Supplier>() { + ServerSetup serverSetup = serverSetup(); + + @Override + public Mono get() { + return transport + .start(duplexConnection -> acceptor(serverSetup, duplexConnection), mtu) + .doOnNext(c -> c.onClose().doFinally(v -> serverSetup.dispose()).subscribe()); + } + }); + } + + private Mono acceptor(ServerSetup serverSetup, DuplexConnection connection) { + ClientServerInputMultiplexer multiplexer = + new ClientServerInputMultiplexer(connection, interceptors, false); + + return multiplexer + .asSetupConnection() + .receive() + .next() + .flatMap(startFrame -> accept(serverSetup, startFrame, multiplexer)); + } + + private Mono acceptResume( + ServerSetup serverSetup, ByteBuf resumeFrame, ClientServerInputMultiplexer multiplexer) { + return serverSetup.acceptRSocketResume(resumeFrame, multiplexer); + } + + private Mono accept( + ServerSetup serverSetup, ByteBuf startFrame, ClientServerInputMultiplexer multiplexer) { + switch (FrameHeaderFlyweight.frameType(startFrame)) { + case SETUP: + return acceptSetup(serverSetup, startFrame, multiplexer); + case RESUME: + return acceptResume(serverSetup, startFrame, multiplexer); + default: + return serverSetup + .sendError( + multiplexer, + new InvalidSetupException( + "invalid setup frame: " + FrameHeaderFlyweight.frameType(startFrame))) + .doFinally( + signalType -> { + startFrame.release(); + multiplexer.dispose(); + }); + } + } + + private Mono acceptSetup( + ServerSetup serverSetup, ByteBuf setupFrame, ClientServerInputMultiplexer multiplexer) { + + if (!SetupFrameFlyweight.isSupportedVersion(setupFrame)) { + return serverSetup + .sendError( + multiplexer, + new InvalidSetupException( + "Unsupported version: " + SetupFrameFlyweight.humanReadableVersion(setupFrame))) + .doFinally( + signalType -> { + setupFrame.release(); + multiplexer.dispose(); + }); + } + + boolean leaseEnabled = leasesSupplier != null; + if (SetupFrameFlyweight.honorLease(setupFrame) && !leaseEnabled) { + return serverSetup + .sendError(multiplexer, new InvalidSetupException("lease is not supported")) + .doFinally( + signalType -> { + setupFrame.release(); + multiplexer.dispose(); + }); + } + + return serverSetup.acceptRSocketSetup( + setupFrame, + multiplexer, + (keepAliveHandler, wrappedMultiplexer) -> { + ConnectionSetupPayload setupPayload = new DefaultConnectionSetupPayload(setupFrame); + + Leases leases = leaseEnabled ? leasesSupplier.get() : null; + RequesterLeaseHandler requesterLeaseHandler = + leaseEnabled + ? new RequesterLeaseHandler.Impl(SERVER_TAG, leases.receiver()) + : RequesterLeaseHandler.None; + + RSocket rSocketRequester = + new RSocketRequester( + allocator, + wrappedMultiplexer.asServerConnection(), + payloadDecoder, + errorConsumer, + StreamIdSupplier.serverSupplier(), + mtu, + setupPayload.keepAliveInterval(), + setupPayload.keepAliveMaxLifetime(), + keepAliveHandler, + requesterLeaseHandler); + + RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); + + return interceptors + .initSocketAcceptor(acceptor) + .accept(setupPayload, wrappedRSocketRequester) + .onErrorResume( + err -> + serverSetup + .sendError(multiplexer, rejectedSetupError(err)) + .then(Mono.error(err))) + .doOnNext( + rSocketHandler -> { + RSocket wrappedRSocketHandler = interceptors.initResponder(rSocketHandler); + + ResponderLeaseHandler responderLeaseHandler = + leaseEnabled + ? new ResponderLeaseHandler.Impl<>( + SERVER_TAG, + allocator, + leases.sender(), + errorConsumer, + leases.stats()) + : ResponderLeaseHandler.None; + + RSocket rSocketResponder = + new RSocketResponder( + allocator, + wrappedMultiplexer.asClientConnection(), + wrappedRSocketHandler, + payloadDecoder, + errorConsumer, + responderLeaseHandler, + mtu); + }) + .doFinally(signalType -> setupPayload.release()) + .then(); + }); + } + + private ServerSetup serverSetup() { + return resume != null ? createSetup() : new ServerSetup.DefaultServerSetup(allocator); + } + + ServerSetup createSetup() { + return new ServerSetup.ResumableServerSetup( + allocator, + new SessionManager(), + resume.getSessionDuration(), + resume.getStreamTimeout(), + resume.getStoreFactory(SERVER_TAG), + resume.isCleanupStoreOnKeepAlive()); + } + + private Exception rejectedSetupError(Throwable err) { + String msg = err.getMessage(); + return new RejectedSetupException(msg == null ? "rejected by server acceptor" : msg); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/core/Resume.java b/rsocket-core/src/main/java/io/rsocket/core/Resume.java new file mode 100644 index 000000000..4b0edab00 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/Resume.java @@ -0,0 +1,96 @@ +/* + * 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 + * + * https://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.core; + +import io.netty.buffer.ByteBuf; +import io.rsocket.frame.ResumeFrameFlyweight; +import io.rsocket.resume.ExponentialBackoffResumeStrategy; +import io.rsocket.resume.InMemoryResumableFramesStore; +import io.rsocket.resume.ResumableFramesStore; +import io.rsocket.resume.ResumeStrategy; +import java.time.Duration; +import java.util.function.Function; +import java.util.function.Supplier; + +public class Resume { + Duration sessionDuration = Duration.ofMinutes(2); + Duration streamTimeout = Duration.ofSeconds(10); + boolean cleanupStoreOnKeepAlive; + Function storeFactory; + + private Supplier tokenSupplier = ResumeFrameFlyweight::generateResumeToken; + private Supplier resumeStrategySupplier = + () -> new ExponentialBackoffResumeStrategy(Duration.ofSeconds(1), Duration.ofSeconds(16), 2); + + public Resume() {} + + public Resume sessionDuration(Duration sessionDuration) { + this.sessionDuration = sessionDuration; + return this; + } + + public Resume streamTimeout(Duration streamTimeout) { + this.streamTimeout = streamTimeout; + return this; + } + + public Resume cleanupStoreOnKeepAlive() { + this.cleanupStoreOnKeepAlive = true; + return this; + } + + public Resume storeFactory( + Function storeFactory) { + this.storeFactory = storeFactory; + return this; + } + + public Resume token(Supplier supplier) { + this.tokenSupplier = supplier; + return this; + } + + public Resume resumeStrategy(Supplier supplier) { + this.resumeStrategySupplier = supplier; + return this; + } + + Duration getSessionDuration() { + return sessionDuration; + } + + Duration getStreamTimeout() { + return streamTimeout; + } + + boolean isCleanupStoreOnKeepAlive() { + return cleanupStoreOnKeepAlive; + } + + Function getStoreFactory(String tag) { + return storeFactory != null + ? storeFactory + : token -> new InMemoryResumableFramesStore(tag, 100_000); + } + + Supplier getTokenSupplier() { + return tokenSupplier; + } + + Supplier getResumeStrategySupplier() { + return resumeStrategySupplier; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index 1e76b6898..68098e279 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -22,7 +22,7 @@ import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameUtil; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; -import io.rsocket.plugins.PluginRegistry; +import io.rsocket.plugins.InitializingInterceptorRegistry; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,7 +45,8 @@ */ public class ClientServerInputMultiplexer implements Closeable { private static final Logger LOGGER = LoggerFactory.getLogger("io.rsocket.FrameLogger"); - private static final PluginRegistry emptyPluginRegistry = new PluginRegistry(); + private static final InitializingInterceptorRegistry emptyInterceptorRegistry = + new InitializingInterceptorRegistry(); private final DuplexConnection setupConnection; private final DuplexConnection serverConnection; @@ -54,23 +55,23 @@ public class ClientServerInputMultiplexer implements Closeable { private final DuplexConnection clientServerConnection; public ClientServerInputMultiplexer(DuplexConnection source) { - this(source, emptyPluginRegistry, false); + this(source, emptyInterceptorRegistry, false); } public ClientServerInputMultiplexer( - DuplexConnection source, PluginRegistry plugins, boolean isClient) { + DuplexConnection source, InitializingInterceptorRegistry registry, boolean isClient) { this.source = source; final MonoProcessor> setup = MonoProcessor.create(); final MonoProcessor> server = MonoProcessor.create(); final MonoProcessor> client = MonoProcessor.create(); - source = plugins.applyConnection(Type.SOURCE, source); + source = registry.initConnection(Type.SOURCE, source); setupConnection = - plugins.applyConnection(Type.SETUP, new InternalDuplexConnection(source, setup)); + registry.initConnection(Type.SETUP, new InternalDuplexConnection(source, setup)); serverConnection = - plugins.applyConnection(Type.SERVER, new InternalDuplexConnection(source, server)); + registry.initConnection(Type.SERVER, new InternalDuplexConnection(source, server)); clientConnection = - plugins.applyConnection(Type.CLIENT, new InternalDuplexConnection(source, client)); + registry.initConnection(Type.CLIENT, new InternalDuplexConnection(source, client)); clientServerConnection = new InternalDuplexConnection(source, client, server); source diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java new file mode 100644 index 000000000..cf911b954 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java @@ -0,0 +1,52 @@ +/* + * 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.plugins; + +import io.rsocket.DuplexConnection; +import io.rsocket.RSocket; +import io.rsocket.SocketAcceptor; + +public class InitializingInterceptorRegistry extends InterceptorRegistry { + + public DuplexConnection initConnection( + DuplexConnectionInterceptor.Type type, DuplexConnection connection) { + for (DuplexConnectionInterceptor interceptor : getConnectionInterceptors()) { + connection = interceptor.apply(type, connection); + } + return connection; + } + + public RSocket initRequester(RSocket rsocket) { + for (RSocketInterceptor interceptor : getRequesterInteceptors()) { + rsocket = interceptor.apply(rsocket); + } + return rsocket; + } + + public RSocket initResponder(RSocket rsocket) { + for (RSocketInterceptor interceptor : getResponderInterceptors()) { + rsocket = interceptor.apply(rsocket); + } + return rsocket; + } + + public SocketAcceptor initSocketAcceptor(SocketAcceptor acceptor) { + for (SocketAcceptorInterceptor interceptor : getSocketAcceptorInterceptors()) { + acceptor = interceptor.apply(acceptor); + } + return acceptor; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java new file mode 100644 index 000000000..f9ee151a8 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java @@ -0,0 +1,83 @@ +/* + * 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.plugins; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +public class InterceptorRegistry { + private List connectionInterceptors = new ArrayList<>(); + private List requesterInteceptors = new ArrayList<>(); + private List responderInterceptors = new ArrayList<>(); + private List socketAcceptorInterceptors = new ArrayList<>(); + + public InterceptorRegistry forConnection(DuplexConnectionInterceptor interceptor) { + connectionInterceptors.add(interceptor); + return this; + } + + public InterceptorRegistry forConnection(Consumer> consumer) { + consumer.accept(connectionInterceptors); + return this; + } + + public InterceptorRegistry forRequester(RSocketInterceptor interceptor) { + requesterInteceptors.add(interceptor); + return this; + } + + public InterceptorRegistry forRequester(Consumer> consumer) { + consumer.accept(requesterInteceptors); + return this; + } + + public InterceptorRegistry forResponder(RSocketInterceptor interceptor) { + responderInterceptors.add(interceptor); + return this; + } + + public InterceptorRegistry forResponder(Consumer> consumer) { + consumer.accept(responderInterceptors); + return this; + } + + public InterceptorRegistry forSocketAcceptor(SocketAcceptorInterceptor interceptor) { + socketAcceptorInterceptors.add(interceptor); + return this; + } + + public InterceptorRegistry forSocketAcceptor(Consumer> consumer) { + consumer.accept(socketAcceptorInterceptors); + return this; + } + + List getConnectionInterceptors() { + return connectionInterceptors; + } + + List getRequesterInteceptors() { + return requesterInteceptors; + } + + List getResponderInterceptors() { + return responderInterceptors; + } + + List getSocketAcceptorInterceptors() { + return socketAcceptorInterceptors; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java deleted file mode 100644 index e3a19367c..000000000 --- a/rsocket-core/src/main/java/io/rsocket/plugins/PluginRegistry.java +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright 2015-2018 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.plugins; - -import io.rsocket.DuplexConnection; -import io.rsocket.RSocket; -import io.rsocket.SocketAcceptor; -import java.util.ArrayList; -import java.util.List; - -public class PluginRegistry { - private List connections = new ArrayList<>(); - private List requesters = new ArrayList<>(); - private List responders = new ArrayList<>(); - private List socketAcceptorInterceptors = new ArrayList<>(); - - public PluginRegistry() {} - - public PluginRegistry(PluginRegistry defaults) { - this.connections.addAll(defaults.connections); - this.requesters.addAll(defaults.requesters); - this.responders.addAll(defaults.responders); - } - - public void addConnectionPlugin(DuplexConnectionInterceptor interceptor) { - connections.add(interceptor); - } - - /** Deprecated. Use {@link #addRequesterPlugin(RSocketInterceptor)} instead */ - @Deprecated - public void addClientPlugin(RSocketInterceptor interceptor) { - addRequesterPlugin(interceptor); - } - - public void addRequesterPlugin(RSocketInterceptor interceptor) { - requesters.add(interceptor); - } - - /** Deprecated. Use {@link #addResponderPlugin(RSocketInterceptor)} instead */ - @Deprecated - public void addServerPlugin(RSocketInterceptor interceptor) { - addResponderPlugin(interceptor); - } - - public void addResponderPlugin(RSocketInterceptor interceptor) { - responders.add(interceptor); - } - - public void addSocketAcceptorPlugin(SocketAcceptorInterceptor interceptor) { - socketAcceptorInterceptors.add(interceptor); - } - - /** Deprecated. Use {@link #applyRequester(RSocket)} instead */ - @Deprecated - public RSocket applyClient(RSocket rSocket) { - return applyRequester(rSocket); - } - - public RSocket applyRequester(RSocket rSocket) { - for (RSocketInterceptor i : requesters) { - rSocket = i.apply(rSocket); - } - - return rSocket; - } - - /** Deprecated. Use {@link #applyResponder(RSocket)} instead */ - @Deprecated - public RSocket applyServer(RSocket rSocket) { - return applyResponder(rSocket); - } - - public RSocket applyResponder(RSocket rSocket) { - for (RSocketInterceptor i : responders) { - rSocket = i.apply(rSocket); - } - - return rSocket; - } - - public SocketAcceptor applySocketAcceptorInterceptor(SocketAcceptor acceptor) { - for (SocketAcceptorInterceptor i : socketAcceptorInterceptors) { - acceptor = i.apply(acceptor); - } - - return acceptor; - } - - public DuplexConnection applyConnection( - DuplexConnectionInterceptor.Type type, DuplexConnection connection) { - for (DuplexConnectionInterceptor i : connections) { - connection = i.apply(type, connection); - } - - return connection; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/Plugins.java b/rsocket-core/src/main/java/io/rsocket/plugins/Plugins.java deleted file mode 100644 index 1ac147687..000000000 --- a/rsocket-core/src/main/java/io/rsocket/plugins/Plugins.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2015-2018 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.plugins; - -/** JVM wide plugins for RSocket */ -public class Plugins { - private static PluginRegistry DEFAULT = new PluginRegistry(); - - private Plugins() {} - - public static void interceptConnection(DuplexConnectionInterceptor interceptor) { - DEFAULT.addConnectionPlugin(interceptor); - } - - public static void interceptClient(RSocketInterceptor interceptor) { - DEFAULT.addClientPlugin(interceptor); - } - - public static void interceptServer(RSocketInterceptor interceptor) { - DEFAULT.addServerPlugin(interceptor); - } - - public static PluginRegistry defaultPlugins() { - return DEFAULT; - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java index a9076428c..01ee1eb6d 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * 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. @@ -19,13 +19,16 @@ import static io.rsocket.frame.FrameType.ERROR; import static io.rsocket.frame.FrameType.SETUP; import static org.assertj.core.data.Offset.offset; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.buffer.UnpooledByteBufAllocator; -import io.rsocket.*; +import io.rsocket.Payload; +import io.rsocket.RSocket; import io.rsocket.exceptions.Exceptions; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; @@ -35,7 +38,7 @@ import io.rsocket.internal.ClientServerInputMultiplexer; import io.rsocket.lease.*; import io.rsocket.lease.MissingLeaseException; -import io.rsocket.plugins.PluginRegistry; +import io.rsocket.plugins.InitializingInterceptorRegistry; import io.rsocket.test.util.TestClientTransport; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestServerTransport; @@ -84,7 +87,7 @@ void setUp() { TAG, byteBufAllocator, stats -> leaseSender, err -> {}, Optional.empty()); ClientServerInputMultiplexer multiplexer = - new ClientServerInputMultiplexer(connection, new PluginRegistry(), true); + new ClientServerInputMultiplexer(connection, new InitializingInterceptorRegistry(), true); rSocketRequester = new RSocketRequester( byteBufAllocator, @@ -130,12 +133,7 @@ public void serverRSocketFactoryRejectsUnsupportedLease() { payload); TestServerTransport transport = new TestServerTransport(); - Closeable server = - RSocketFactory.receive() - .acceptor((setup, sendingSocket) -> Mono.just(new AbstractRSocket() {})) - .transport(transport) - .start() - .block(); + RSocketServer.create().bind(transport).block(); TestDuplexConnection connection = transport.connect(); connection.addToReceivedBuffer(setupFrame); @@ -151,7 +149,7 @@ public void serverRSocketFactoryRejectsUnsupportedLease() { @Test public void clientRSocketFactorySetsLeaseFlag() { TestClientTransport clientTransport = new TestClientTransport(); - RSocketFactory.connect().lease().transport(clientTransport).start().block(); + RSocketConnector.create().lease(Leases::new).connect(clientTransport).block(); Collection sent = clientTransport.testConnection().getSent(); Assertions.assertThat(sent).hasSize(1); diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java index f7ba12ada..dc76b5450 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketReconnectTest.java @@ -18,7 +18,6 @@ import static org.junit.Assert.assertEquals; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; import io.rsocket.test.util.TestClientTransport; import io.rsocket.transport.ClientTransport; import java.io.UncheckedIOException; @@ -27,7 +26,6 @@ import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.function.Consumer; -import java.util.function.Supplier; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -44,14 +42,9 @@ public void shouldBeASharedReconnectableInstanceOfRSocketMono() { TestClientTransport[] testClientTransport = new TestClientTransport[] {new TestClientTransport()}; Mono rSocketMono = - RSocketFactory.connect() - .singleSubscriberRequester() + RSocketConnector.create() .reconnect(Retry.indefinitely()) - .transport( - () -> { - return testClientTransport[0]; - }) - .start(); + .connect(() -> testClientTransport[0]); RSocket rSocket1 = rSocketMono.block(); RSocket rSocket2 = rSocketMono.block(); @@ -70,22 +63,20 @@ public void shouldBeASharedReconnectableInstanceOfRSocketMono() { @Test @SuppressWarnings({"rawtype", "unchecked"}) public void shouldBeRetrieableConnectionSharedReconnectableInstanceOfRSocketMono() { - Supplier mockTransportSupplier = Mockito.mock(Supplier.class); - Mockito.when(mockTransportSupplier.get()) + ClientTransport transport = Mockito.mock(ClientTransport.class); + Mockito.when(transport.connect(0)) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) - .thenReturn(new TestClientTransport()); + .thenReturn(new TestClientTransport().connect(0)); Mono rSocketMono = - RSocketFactory.connect() - .singleSubscriberRequester() + RSocketConnector.create() .reconnect( Retry.backoff(4, Duration.ofMillis(100)) .maxBackoff(Duration.ofMillis(500)) .doAfterRetry(onRetry())) - .transport(mockTransportSupplier) - .start(); + .connect(transport); RSocket rSocket1 = rSocketMono.block(); RSocket rSocket2 = rSocketMono.block(); @@ -101,23 +92,21 @@ public void shouldBeRetrieableConnectionSharedReconnectableInstanceOfRSocketMono @Test @SuppressWarnings({"rawtype", "unchecked"}) public void shouldBeExaustedRetrieableConnectionSharedReconnectableInstanceOfRSocketMono() { - Supplier mockTransportSupplier = Mockito.mock(Supplier.class); - Mockito.when(mockTransportSupplier.get()) + ClientTransport transport = Mockito.mock(ClientTransport.class); + Mockito.when(transport.connect(0)) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) .thenThrow(UncheckedIOException.class) - .thenReturn(new TestClientTransport()); + .thenReturn(new TestClientTransport().connect(0)); Mono rSocketMono = - RSocketFactory.connect() - .singleSubscriberRequester() + RSocketConnector.create() .reconnect( Retry.backoff(4, Duration.ofMillis(100)) .maxBackoff(Duration.ofMillis(500)) .doAfterRetry(onRetry())) - .transport(mockTransportSupplier) - .start(); + .connect(transport); Assertions.assertThatThrownBy(rSocketMono::block) .matches(Exceptions::isRetryExhausted) @@ -137,11 +126,7 @@ public void shouldBeExaustedRetrieableConnectionSharedReconnectableInstanceOfRSo @Test public void shouldBeNotBeASharedReconnectableInstanceOfRSocketMono() { - Mono rSocketMono = - RSocketFactory.connect() - .singleSubscriberRequester() - .transport(new TestClientTransport()) - .start(); + Mono rSocketMono = RSocketConnector.connectWith(new TestClientTransport()); RSocket rSocket1 = rSocketMono.block(); RSocket rSocket2 = rSocketMono.block(); diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java new file mode 100644 index 000000000..9d105a8c9 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java @@ -0,0 +1,43 @@ +package io.rsocket.core; + +import io.rsocket.test.util.TestClientTransport; +import io.rsocket.test.util.TestServerTransport; +import org.assertj.core.api.Assertions; +import org.junit.Test; + +public class RSocketServerFragmentationTest { + + @Test + public void serverErrorsWithEnabledFragmentationOnInsufficientMtu() { + Assertions.assertThatIllegalArgumentException() + .isThrownBy(() -> RSocketServer.create().fragment(2)) + .withMessage("smallest allowed mtu size is 64 bytes, provided: 2"); + } + + @Test + public void serverSucceedsWithEnabledFragmentationOnSufficientMtu() { + RSocketServer.create().fragment(100).bind(new TestServerTransport()).block(); + } + + @Test + public void serverSucceedsWithDisabledFragmentation() { + RSocketServer.create().bind(new TestServerTransport()).block(); + } + + @Test + public void clientErrorsWithEnabledFragmentationOnInsufficientMtu() { + Assertions.assertThatIllegalArgumentException() + .isThrownBy(() -> RSocketConnector.create().fragment(2)) + .withMessage("smallest allowed mtu size is 64 bytes, provided: 2"); + } + + @Test + public void clientSucceedsWithEnabledFragmentationOnSufficientMtu() { + RSocketConnector.create().fragment(100).connect(TestClientTransport::new).block(); + } + + @Test + public void clientSucceedsWithDisabledFragmentation() { + RSocketConnector.connectWith(new TestClientTransport()).block(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java index 9344d69da..5b489896b 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -32,7 +32,7 @@ void responderRejectSetup() { String errorMsg = "error"; RejectingAcceptor acceptor = new RejectingAcceptor(errorMsg); - RSocketFactory.receive().acceptor(acceptor).transport(transport).start().block(); + RSocketServer.create().acceptor(acceptor).bind(transport).block(); transport.connect(); diff --git a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java index 8f56608d8..9a4b5d2db 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java @@ -22,7 +22,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.rsocket.frame.*; -import io.rsocket.plugins.PluginRegistry; +import io.rsocket.plugins.InitializingInterceptorRegistry; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.DefaultPayload; import java.util.concurrent.atomic.AtomicInteger; @@ -38,8 +38,10 @@ public class ClientServerInputMultiplexerTest { @Before public void setup() { source = new TestDuplexConnection(); - clientMultiplexer = new ClientServerInputMultiplexer(source, new PluginRegistry(), true); - serverMultiplexer = new ClientServerInputMultiplexer(source, new PluginRegistry(), false); + clientMultiplexer = + new ClientServerInputMultiplexer(source, new InitializingInterceptorRegistry(), true); + serverMultiplexer = + new ClientServerInputMultiplexer(source, new InitializingInterceptorRegistry(), false); } @Test 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 ac889ecfc..44daf8c67 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -20,67 +20,49 @@ import io.rsocket.ConnectionSetupPayload; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; import io.rsocket.SocketAcceptor; -import io.rsocket.frame.decoder.PayloadDecoder; -import io.rsocket.transport.local.LocalClientTransport; -import io.rsocket.transport.local.LocalServerTransport; -import io.rsocket.util.ByteBufPayload; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.DefaultPayload; import java.time.Duration; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; public final class ChannelEchoClient { - static final Payload payload1 = ByteBufPayload.create("Hello "); public static void main(String[] args) { - RSocketFactory.receive() - .frameDecoder(PayloadDecoder.ZERO_COPY) - .acceptor(new SocketAcceptorImpl()) - .transport(LocalServerTransport.create("localhost")) - .start() + RSocketServer.create(new EchoAcceptor()) + .bind(TcpServerTransport.create("localhost", 7000)) .subscribe(); RSocket socket = - RSocketFactory.connect() - .keepAliveAckTimeout(Duration.ofMinutes(10)) - .frameDecoder(PayloadDecoder.ZERO_COPY) - .transport(LocalClientTransport.create("localhost")) - .start() - .block(); + RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); - Flux.range(0, 100000000) - .concatMap(i -> socket.fireAndForget(payload1.retain())) - // .doOnNext(p -> { - //// System.out.println(p.getDataUtf8()); - // p.release(); - // }) - .blockLast(); + socket + .requestChannel( + Flux.interval(Duration.ofMillis(1000)).map(i -> DefaultPayload.create("Hello"))) + .map(Payload::getDataUtf8) + .doOnNext(System.out::println) + .take(10) + .doFinally(signalType -> socket.dispose()) + .then() + .block(); } - private static class SocketAcceptorImpl implements SocketAcceptor { + private static class EchoAcceptor implements SocketAcceptor { @Override public Mono accept(ConnectionSetupPayload setupPayload, RSocket reactiveSocket) { return Mono.just( new AbstractRSocket() { - - @Override - public Mono fireAndForget(Payload payload) { - // System.out.println(payload.getDataUtf8()); - payload.release(); - return Mono.empty(); - } - - @Override - public Mono requestResponse(Payload payload) { - return Mono.just(payload); - } - @Override public Flux requestChannel(Publisher payloads) { - return Flux.from(payloads).subscribeOn(Schedulers.single()); + return Flux.from(payloads) + .map(Payload::getDataUtf8) + .map(s -> "Echo: " + s) + .map(DefaultPayload::create); } }); } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/duplex/DuplexClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/duplex/DuplexClient.java index c0a271d66..bfa58bf40 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/duplex/DuplexClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/duplex/DuplexClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -19,7 +19,8 @@ import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; @@ -30,10 +31,9 @@ public final class DuplexClient { public static void main(String[] args) { - RSocketFactory.receive() - .acceptor( - (setup, reactiveSocket) -> { - reactiveSocket + RSocketServer.create( + (setup, rsocket) -> { + rsocket .requestStream(DefaultPayload.create("Hello-Bidi")) .map(Payload::getDataUtf8) .log() @@ -41,23 +41,22 @@ public static void main(String[] args) { return Mono.just(new AbstractRSocket() {}); }) - .transport(TcpServerTransport.create("localhost", 7000)) - .start() + .bind(TcpServerTransport.create("localhost", 7000)) .subscribe(); RSocket socket = - RSocketFactory.connect() + RSocketConnector.create() .acceptor( - rSocket -> - new AbstractRSocket() { - @Override - public Flux requestStream(Payload payload) { - return Flux.interval(Duration.ofSeconds(1)) - .map(aLong -> DefaultPayload.create("Bi-di Response => " + aLong)); - } - }) - .transport(TcpClientTransport.create("localhost", 7000)) - .start() + (setup, rsocket) -> + Mono.just( + new AbstractRSocket() { + @Override + public Flux requestStream(Payload payload) { + return Flux.interval(Duration.ofSeconds(1)) + .map(aLong -> DefaultPayload.create("Bi-di Response => " + aLong)); + } + })) + .connect(TcpClientTransport.create("localhost", 7000)) .block(); socket.onClose().block(); diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java index 7482c7d1a..a12c9a170 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java @@ -1,3 +1,19 @@ +/* + * 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.tcp.lease; import static java.time.Duration.ofSeconds; @@ -5,7 +21,8 @@ import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.lease.Lease; import io.rsocket.lease.LeaseStats; import io.rsocket.lease.Leases; @@ -27,28 +44,26 @@ public class LeaseExample { public static void main(String[] args) { CloseableChannel server = - RSocketFactory.receive() + RSocketServer.create( + (setup, sendingRSocket) -> Mono.just(new ServerAcceptor(sendingRSocket))) .lease( () -> Leases.create() .sender(new LeaseSender(SERVER_TAG, 7_000, 5)) .receiver(new LeaseReceiver(SERVER_TAG)) .stats(new NoopStats())) - .acceptor((setup, sendingRSocket) -> Mono.just(new ServerAcceptor(sendingRSocket))) - .transport(TcpServerTransport.create("localhost", 7000)) - .start() + .bind(TcpServerTransport.create("localhost", 7000)) .block(); RSocket clientRSocket = - RSocketFactory.connect() + RSocketConnector.create() .lease( () -> Leases.create() .sender(new LeaseSender(CLIENT_TAG, 3_000, 5)) .receiver(new LeaseReceiver(CLIENT_TAG))) - .acceptor(rSocket -> new ClientAcceptor()) - .transport(TcpClientTransport.create(server.address())) - .start() + .acceptor((rSocket, setup) -> Mono.just(new ClientAcceptor())) + .connect(TcpClientTransport.create(server.address())) .block(); Flux.interval(ofSeconds(1)) 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 537485fa4..26a79c4e1 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -19,7 +19,8 @@ import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; @@ -28,8 +29,7 @@ public final class HelloWorldClient { public static void main(String[] args) { - RSocketFactory.receive() - .acceptor( + RSocketServer.create( (setupPayload, reactiveSocket) -> Mono.just( new AbstractRSocket() { @@ -45,15 +45,11 @@ public Mono requestResponse(Payload p) { } } })) - .transport(TcpServerTransport.create("localhost", 7000)) - .start() + .bind(TcpServerTransport.create("localhost", 7000)) .subscribe(); RSocket socket = - RSocketFactory.connect() - .transport(TcpClientTransport.create("localhost", 7000)) - .start() - .block(); + RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); socket .requestResponse(DefaultPayload.create("Hello")) 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 ca115d281..3cae64409 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 @@ -1,9 +1,27 @@ +/* + * 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.tcp.resume; import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.core.Resume; import io.rsocket.resume.ClientResume; import io.rsocket.resume.PeriodicResumeStrategy; import io.rsocket.resume.ResumeStrategy; @@ -22,24 +40,22 @@ public class ResumeFileTransfer { public static void main(String[] args) { RequestCodec requestCodec = new RequestCodec(); + Resume resume = + new Resume() + .sessionDuration(Duration.ofMinutes(5)) + .resumeStrategy( + () -> new VerboseResumeStrategy(new PeriodicResumeStrategy(Duration.ofSeconds(1)))); CloseableChannel server = - RSocketFactory.receive() - .resume() - .resumeSessionDuration(Duration.ofMinutes(5)) - .acceptor((setup, rSocket) -> Mono.just(new FileServer(requestCodec))) - .transport(TcpServerTransport.create("localhost", 8000)) - .start() + RSocketServer.create((setup, rSocket) -> Mono.just(new FileServer(requestCodec))) + .resume(resume) + .bind(TcpServerTransport.create("localhost", 8000)) .block(); RSocket client = - RSocketFactory.connect() - .resume() - .resumeStrategy( - () -> new VerboseResumeStrategy(new PeriodicResumeStrategy(Duration.ofSeconds(1)))) - .resumeSessionDuration(Duration.ofMinutes(5)) - .transport(TcpClientTransport.create("localhost", 8001)) - .start() + RSocketConnector.create() + .resume(resume) + .connect(TcpClientTransport.create("localhost", 8001)) .block(); client diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java index 57a659c1d..5f7c777db 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,7 +16,13 @@ package io.rsocket.examples.transport.tcp.stream; -import io.rsocket.*; +import io.rsocket.AbstractRSocket; +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.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; @@ -27,17 +33,12 @@ public final class StreamingClient { public static void main(String[] args) { - RSocketFactory.receive() - .acceptor(new SocketAcceptorImpl()) - .transport(TcpServerTransport.create("localhost", 7000)) - .start() + RSocketServer.create(new SocketAcceptorImpl()) + .bind(TcpServerTransport.create("localhost", 7000)) .subscribe(); RSocket socket = - RSocketFactory.connect() - .transport(TcpClientTransport.create("localhost", 7000)) - .start() - .block(); + RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); socket .requestStream(DefaultPayload.create("Hello")) 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 d3865c01b..908505a2f 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -22,8 +22,9 @@ import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.WebsocketDuplexConnection; @@ -45,10 +46,10 @@ public class WebSocketHeadersSample { public static void main(String[] args) { ServerTransport.ConnectionAcceptor acceptor = - RSocketFactory.receive() - .frameDecoder(PayloadDecoder.ZERO_COPY) + RSocketServer.create() + .payloadDecoder(PayloadDecoder.ZERO_COPY) .acceptor(new SocketAcceptorImpl()) - .toConnectionAcceptor(); + .asConnectionAcceptor(); DisposableServer disposableServer = HttpServer.create() @@ -82,11 +83,10 @@ public static void main(String[] args) { }); RSocket socket = - RSocketFactory.connect() - .keepAliveAckTimeout(Duration.ofMinutes(10)) - .frameDecoder(PayloadDecoder.ZERO_COPY) - .transport(clientTransport) - .start() + RSocketConnector.create() + .keepAlive(Duration.ofMinutes(10), Duration.ofMinutes(10)) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(clientTransport) .block(); Flux.range(0, 100) @@ -102,11 +102,10 @@ public static void main(String[] args) { WebsocketClientTransport.create(disposableServer.host(), disposableServer.port()); RSocket rSocket = - RSocketFactory.connect() - .keepAliveAckTimeout(Duration.ofMinutes(10)) - .frameDecoder(PayloadDecoder.ZERO_COPY) - .transport(clientTransport2) - .start() + RSocketConnector.create() + .keepAlive(Duration.ofMinutes(10), Duration.ofMinutes(10)) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(clientTransport2) .block(); // expect error here because of closed channel diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java index 19c29061b..1ef7771cd 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java @@ -26,7 +26,8 @@ import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.plugins.DuplexConnectionInterceptor; import io.rsocket.plugins.RSocketInterceptor; import io.rsocket.plugins.SocketAcceptorInterceptor; @@ -39,7 +40,6 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import org.junit.After; -import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.reactivestreams.Publisher; @@ -49,19 +49,20 @@ public class IntegrationTest { - private static final RSocketInterceptor requesterPlugin; - private static final RSocketInterceptor responderPlugin; - private static final SocketAcceptorInterceptor clientAcceptorPlugin; - private static final SocketAcceptorInterceptor serverAcceptorPlugin; - private static final DuplexConnectionInterceptor connectionPlugin; - public static volatile boolean calledRequester = false; - public static volatile boolean calledResponder = false; - public static volatile boolean calledClientAcceptor = false; - public static volatile boolean calledServerAcceptor = false; - public static volatile boolean calledFrame = false; + private static final RSocketInterceptor requesterInterceptor; + private static final RSocketInterceptor responderInterceptor; + private static final SocketAcceptorInterceptor clientAcceptorInterceptor; + private static final SocketAcceptorInterceptor serverAcceptorInterceptor; + private static final DuplexConnectionInterceptor connectionInterceptor; + + private static volatile boolean calledRequester = false; + private static volatile boolean calledResponder = false; + private static volatile boolean calledClientAcceptor = false; + private static volatile boolean calledServerAcceptor = false; + private static volatile boolean calledFrame = false; static { - requesterPlugin = + requesterInterceptor = reactiveSocket -> new RSocketProxy(reactiveSocket) { @Override @@ -71,7 +72,7 @@ public Mono requestResponse(Payload payload) { } }; - responderPlugin = + responderInterceptor = reactiveSocket -> new RSocketProxy(reactiveSocket) { @Override @@ -81,21 +82,21 @@ public Mono requestResponse(Payload payload) { } }; - clientAcceptorPlugin = + clientAcceptorInterceptor = acceptor -> (setup, sendingSocket) -> { calledClientAcceptor = true; return acceptor.accept(setup, sendingSocket); }; - serverAcceptorPlugin = + serverAcceptorInterceptor = acceptor -> (setup, sendingSocket) -> { calledServerAcceptor = true; return acceptor.accept(setup, sendingSocket); }; - connectionPlugin = + connectionInterceptor = (type, connection) -> { calledFrame = true; return connection; @@ -114,18 +115,8 @@ public void startup() { requestCount = new AtomicInteger(); disconnectionCounter = new CountDownLatch(1); - TcpServerTransport serverTransport = TcpServerTransport.create("localhost", 0); - server = - RSocketFactory.receive() - .addResponderPlugin(responderPlugin) - .addSocketAcceptorPlugin(serverAcceptorPlugin) - .addConnectionPlugin(connectionPlugin) - .errorConsumer( - t -> { - errorCount.incrementAndGet(); - }) - .acceptor( + RSocketServer.create( (setup, sendingSocket) -> { sendingSocket .onClose() @@ -152,17 +143,24 @@ public Flux requestChannel(Publisher payloads) { } }); }) - .transport(serverTransport) - .start() + .interceptors( + registry -> + registry + .forResponder(responderInterceptor) + .forSocketAcceptor(serverAcceptorInterceptor) + .forConnection(connectionInterceptor)) + .bind(TcpServerTransport.create("localhost", 0)) .block(); client = - RSocketFactory.connect() - .addRequesterPlugin(requesterPlugin) - .addSocketAcceptorPlugin(clientAcceptorPlugin) - .addConnectionPlugin(connectionPlugin) - .transport(TcpClientTransport.create(server.address())) - .start() + RSocketConnector.create() + .interceptors( + registry -> + registry + .forRequester(requesterInterceptor) + .forSocketAcceptor(clientAcceptorInterceptor) + .forConnection(connectionInterceptor)) + .connect(TcpClientTransport.create(server.address())) .block(); } @@ -204,8 +202,6 @@ public void testCallRequestWithErrorAndThenRequest() { } catch (Throwable t) { } - Assert.assertEquals(1, errorCount.incrementAndGet()); - testRequest(); } } diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java index 7a30a7fd1..d24083ea6 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java @@ -3,7 +3,8 @@ import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.test.SlowTest; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.CloseableChannel; @@ -21,25 +22,20 @@ public class InteractionsLoadTest { @Test @SlowTest public void channel() { - TcpServerTransport serverTransport = TcpServerTransport.create("localhost", 0); - CloseableChannel server = - RSocketFactory.receive() - .acceptor((setup, rsocket) -> Mono.just(new EchoRSocket())) - .transport(serverTransport) - .start() + RSocketServer.create((setup, rsocket) -> Mono.just(new EchoRSocket())) + .bind(TcpServerTransport.create("localhost", 0)) .block(Duration.ofSeconds(10)); - TcpClientTransport transport = TcpClientTransport.create(server.address()); - - RSocket client = - RSocketFactory.connect().transport(transport).start().block(Duration.ofSeconds(10)); + RSocket clientRSocket = + RSocketConnector.connectWith(TcpClientTransport.create(server.address())) + .block(Duration.ofSeconds(10)); int concurrency = 16; Flux.range(1, concurrency) .flatMap( v -> - client + clientRSocket .requestChannel( input().onBackpressureDrop().map(iv -> DefaultPayload.create("foo"))) .limitRate(10000), diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java index 9e7f5b0a7..7133820ca 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java @@ -22,7 +22,8 @@ import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +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; @@ -46,20 +47,14 @@ public class TcpIntegrationTest { @Before public void startup() { - TcpServerTransport serverTransport = TcpServerTransport.create("localhost", 0); server = - RSocketFactory.receive() - .acceptor((setup, sendingSocket) -> Mono.just(new RSocketProxy(handler))) - .transport(serverTransport) - .start() + RSocketServer.create((setup, sendingSocket) -> Mono.just(new RSocketProxy(handler))) + .bind(TcpServerTransport.create("localhost", 0)) .block(); } private RSocket buildClient() { - return RSocketFactory.connect() - .transport(TcpClientTransport.create(server.address())) - .start() - .block(); + return RSocketConnector.connectWith(TcpClientTransport.create(server.address())).block(); } @After diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java index ec1d41bf9..8fe09430a 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,60 +16,52 @@ package io.rsocket.integration; -import io.rsocket.*; +import io.rsocket.AbstractRSocket; +import io.rsocket.Closeable; +import io.rsocket.Payload; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.exceptions.ApplicationErrorException; -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; import io.rsocket.transport.local.LocalClientTransport; import io.rsocket.transport.local.LocalServerTransport; import io.rsocket.util.DefaultPayload; import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Supplier; import org.junit.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class TestingStreaming { - private Supplier> serverSupplier = - () -> LocalServerTransport.create("test"); - - private Supplier clientSupplier = () -> LocalClientTransport.create("test"); + LocalServerTransport serverTransport = LocalServerTransport.create("test"); @Test(expected = ApplicationErrorException.class) public void testRangeButThrowException() { Closeable server = null; try { server = - RSocketFactory.receive() - .errorConsumer(Throwable::printStackTrace) - .acceptor( - (connectionSetupPayload, rSocket) -> { - AbstractRSocket abstractRSocket = - new AbstractRSocket() { - @Override - public double availability() { - return 1.0; - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.range(1, 1000) - .doOnNext( - i -> { - if (i > 3) { - throw new RuntimeException("BOOM!"); - } - }) - .map(l -> DefaultPayload.create("l -> " + l)) - .cast(Payload.class); - } - }; - - return Mono.just(abstractRSocket); - }) - .transport(serverSupplier.get()) - .start() + RSocketServer.create( + (connectionSetupPayload, rSocket) -> + Mono.just( + new AbstractRSocket() { + @Override + public double availability() { + return 1.0; + } + + @Override + public Flux requestStream(Payload payload) { + return Flux.range(1, 1000) + .doOnNext( + i -> { + if (i > 3) { + throw new RuntimeException("BOOM!"); + } + }) + .map(l -> DefaultPayload.create("l -> " + l)) + .cast(Payload.class); + } + })) + .bind(serverTransport) .block(); Flux.range(1, 6).flatMap(i -> consumer("connection number -> " + i)).blockLast(); @@ -85,29 +77,23 @@ public void testRangeOfConsumers() { Closeable server = null; try { server = - RSocketFactory.receive() - .errorConsumer(Throwable::printStackTrace) - .acceptor( - (connectionSetupPayload, rSocket) -> { - AbstractRSocket abstractRSocket = - new AbstractRSocket() { - @Override - public double availability() { - return 1.0; - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.range(1, 1000) - .map(l -> DefaultPayload.create("l -> " + l)) - .cast(Payload.class); - } - }; - - return Mono.just(abstractRSocket); - }) - .transport(serverSupplier.get()) - .start() + RSocketServer.create( + (connectionSetupPayload, rSocket) -> + Mono.just( + new AbstractRSocket() { + @Override + public double availability() { + return 1.0; + } + + @Override + public Flux requestStream(Payload payload) { + return Flux.range(1, 1000) + .map(l -> DefaultPayload.create("l -> " + l)) + .cast(Payload.class); + } + })) + .bind(serverTransport) .block(); Flux.range(1, 6).flatMap(i -> consumer("connection number -> " + i)).blockLast(); @@ -119,10 +105,7 @@ public Flux requestStream(Payload payload) { } private Flux consumer(String s) { - return RSocketFactory.connect() - .errorConsumer(Throwable::printStackTrace) - .transport(clientSupplier) - .start() + return RSocketConnector.connectWith(LocalClientTransport.create("test")) .flatMapMany( rSocket -> { AtomicInteger count = new AtomicInteger(); @@ -135,31 +118,25 @@ private Flux consumer(String s) { @Test public void testSingleConsumer() { Closeable server = null; - try { server = - RSocketFactory.receive() - .acceptor( - (connectionSetupPayload, rSocket) -> { - AbstractRSocket abstractRSocket = - new AbstractRSocket() { - @Override - public double availability() { - return 1.0; - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.range(1, 10_000) - .map(l -> DefaultPayload.create("l -> " + l)) - .cast(Payload.class); - } - }; - - return Mono.just(abstractRSocket); - }) - .transport(serverSupplier.get()) - .start() + RSocketServer.create( + (connectionSetupPayload, rSocket) -> + Mono.just( + new AbstractRSocket() { + @Override + public double availability() { + return 1.0; + } + + @Override + public Flux requestStream(Payload payload) { + return Flux.range(1, 10_000) + .map(l -> DefaultPayload.create("l -> " + l)) + .cast(Payload.class); + } + })) + .bind(serverTransport) .block(); consumer("1").blockLast(); diff --git a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java index 009d0d8db..685cef41e 100644 --- a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * 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. @@ -19,7 +19,9 @@ import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; +import io.rsocket.core.Resume; import io.rsocket.exceptions.RejectedResumeException; import io.rsocket.exceptions.UnsupportedSetupException; import io.rsocket.test.SlowTest; @@ -106,8 +108,7 @@ public void reconnectOnMissingSession() { ErrorConsumer errorConsumer = new ErrorConsumer(); int clientSessionDurationSeconds = 10; - RSocket rSocket = - newClientRSocket(clientTransport, clientSessionDurationSeconds, errorConsumer).block(); + RSocket rSocket = newClientRSocket(clientTransport, clientSessionDurationSeconds).block(); Mono.delay(Duration.ofSeconds(1)) .subscribe(v -> clientTransport.disconnectFor(Duration.ofSeconds(3))); @@ -129,20 +130,16 @@ public void reconnectOnMissingSession() { @Test void serverMissingResume() { CloseableChannel closeableChannel = - RSocketFactory.receive() - .acceptor((setupPayload, rSocket) -> Mono.just(new TestResponderRSocket())) - .transport(serverTransport(SERVER_HOST, SERVER_PORT)) - .start() + RSocketServer.create((setupPayload, rSocket) -> Mono.just(new TestResponderRSocket())) + .bind(serverTransport(SERVER_HOST, SERVER_PORT)) .block(); ErrorConsumer errorConsumer = new ErrorConsumer(); RSocket rSocket = - RSocketFactory.connect() - .resume() - .errorConsumer(errorConsumer) - .transport(clientTransport(closeableChannel.address())) - .start() + RSocketConnector.create() + .resume(new Resume()) + .connect(clientTransport(closeableChannel.address())) .block(); StepVerifier.create(errorConsumer.errors().next().doFinally(s -> closeableChannel.dispose())) @@ -201,24 +198,15 @@ private void throwOnNonContinuous(AtomicInteger counter, String x) { private static Mono newClientRSocket( DisconnectableClientTransport clientTransport, int sessionDurationSeconds) { - return newClientRSocket(clientTransport, sessionDurationSeconds, err -> {}); - } - - private static Mono newClientRSocket( - DisconnectableClientTransport clientTransport, - int sessionDurationSeconds, - Consumer errConsumer) { - return RSocketFactory.connect() - .resume() - .resumeSessionDuration(Duration.ofSeconds(sessionDurationSeconds)) - .resumeStore(t -> new InMemoryResumableFramesStore("client", 500_000)) - .resumeCleanupOnKeepAlive() - .keepAliveTickPeriod(Duration.ofSeconds(5)) - .keepAliveAckTimeout(Duration.ofMinutes(5)) - .errorConsumer(errConsumer) - .resumeStrategy(() -> new PeriodicResumeStrategy(Duration.ofSeconds(1))) - .transport(clientTransport) - .start(); + return RSocketConnector.create() + .resume( + new Resume() + .sessionDuration(Duration.ofSeconds(sessionDurationSeconds)) + .storeFactory(t -> new InMemoryResumableFramesStore("client", 500_000)) + .cleanupStoreOnKeepAlive() + .resumeStrategy(() -> new PeriodicResumeStrategy(Duration.ofSeconds(1)))) + .keepAlive(Duration.ofSeconds(5), Duration.ofMinutes(5)) + .connect(clientTransport); } private static Mono newServerRSocket() { @@ -226,14 +214,13 @@ private static Mono newServerRSocket() { } private static Mono newServerRSocket(int sessionDurationSeconds) { - return RSocketFactory.receive() - .resume() - .resumeStore(t -> new InMemoryResumableFramesStore("server", 500_000)) - .resumeSessionDuration(Duration.ofSeconds(sessionDurationSeconds)) - .resumeCleanupOnKeepAlive() - .acceptor((setupPayload, rSocket) -> Mono.just(new TestResponderRSocket())) - .transport(serverTransport(SERVER_HOST, SERVER_PORT)) - .start(); + return RSocketServer.create((setup, rsocket) -> Mono.just(new TestResponderRSocket())) + .resume( + new Resume() + .sessionDuration(Duration.ofSeconds(sessionDurationSeconds)) + .cleanupStoreOnKeepAlive() + .storeFactory(t -> new InMemoryResumableFramesStore("server", 500_000))) + .bind(serverTransport(SERVER_HOST, SERVER_PORT)); } private static class TestResponderRSocket extends AbstractRSocket { diff --git a/rsocket-test/src/main/java/io/rsocket/test/ClientSetupRule.java b/rsocket-test/src/main/java/io/rsocket/test/ClientSetupRule.java index ec143b7ab..6f562875f 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/ClientSetupRule.java +++ b/rsocket-test/src/main/java/io/rsocket/test/ClientSetupRule.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -18,7 +18,8 @@ import io.rsocket.Closeable; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import java.util.function.BiFunction; @@ -47,17 +48,13 @@ public ClientSetupRule( this.serverInit = address -> - RSocketFactory.receive() - .acceptor((setup, sendingSocket) -> Mono.just(new TestRSocket(data, metadata))) - .transport(serverTransportSupplier.apply(address)) - .start() + RSocketServer.create((setup, rsocket) -> Mono.just(new TestRSocket(data, metadata))) + .bind(serverTransportSupplier.apply(address)) .block(); this.clientConnector = (address, server) -> - RSocketFactory.connect() - .transport(clientTransportSupplier.apply(address, server)) - .start() + RSocketConnector.connectWith(clientTransportSupplier.apply(address, server)) .doOnError(Throwable::printStackTrace) .block(); } diff --git a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java index 1c00b0502..e4ff75b2a 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -19,7 +19,8 @@ import io.rsocket.Closeable; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.util.DefaultPayload; @@ -380,17 +381,12 @@ public TransportPair( T address = addressSupplier.get(); server = - RSocketFactory.receive() - .acceptor((setup, sendingSocket) -> Mono.just(new TestRSocket(data, metadata))) - .transport(serverTransportSupplier.apply(address)) - .start() + RSocketServer.create((setup, sendingSocket) -> Mono.just(new TestRSocket(data, metadata))) + .bind(serverTransportSupplier.apply(address)) .block(); client = - RSocketFactory.connect() - .keepAlive(Duration.ZERO, Duration.ZERO, 1) - .transport(clientTransportSupplier.apply(address, server)) - .start() + RSocketConnector.connectWith(clientTransportSupplier.apply(address, server)) .doOnError(Throwable::printStackTrace) .block(); } diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalPingPong.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalPingPong.java index 2e4f93ac4..9228e2d05 100644 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalPingPong.java +++ b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalPingPong.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -17,7 +17,8 @@ package io.rsocket.transport.local; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.test.PingClient; import io.rsocket.test.PingHandler; @@ -28,18 +29,15 @@ public final class LocalPingPong { public static void main(String... args) { - RSocketFactory.receive() - .frameDecoder(PayloadDecoder.ZERO_COPY) - .acceptor(new PingHandler()) - .transport(LocalServerTransport.create("test-local-server")) - .start() + RSocketServer.create(new PingHandler()) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(LocalServerTransport.create("test-local-server")) .block(); Mono client = - RSocketFactory.connect() - .frameDecoder(PayloadDecoder.ZERO_COPY) - .transport(LocalClientTransport.create("test-local-server")) - .start(); + RSocketConnector.create() + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(LocalClientTransport.create("test-local-server")); PingClient pingClient = new PingClient(client); diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java index 575993c18..db2f07bda 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -21,7 +21,8 @@ import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +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; @@ -59,19 +60,16 @@ public void startup() { TcpServerTransport serverTransport = TcpServerTransport.create("localhost", randomPort); server = - RSocketFactory.receive() + RSocketServer.create((setup, sendingSocket) -> Mono.just(new RSocketProxy(handler))) .fragment(frameSize) - .acceptor((setup, sendingSocket) -> Mono.just(new RSocketProxy(handler))) - .transport(serverTransport) - .start() + .bind(serverTransport) .block(); } private RSocket buildClient() { - return RSocketFactory.connect() + return RSocketConnector.create() .fragment(frameSize) - .transport(TcpClientTransport.create(server.address())) - .start() + .connect(TcpClientTransport.create(server.address())) .block(); } diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/RSocketFactoryNettyTransportFragmentationTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/RSocketFactoryNettyTransportFragmentationTest.java index 07e9378fa..b9c0d4f60 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/RSocketFactoryNettyTransportFragmentationTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/RSocketFactoryNettyTransportFragmentationTest.java @@ -1,18 +1,15 @@ package io.rsocket.transport.netty; -import static io.rsocket.RSocketFactory.*; - import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; import io.rsocket.SocketAcceptor; -import io.rsocket.transport.ClientTransport; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.CloseableChannel; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.transport.netty.server.WebsocketServerTransport; import java.time.Duration; -import java.util.function.Function; import java.util.stream.Stream; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -22,104 +19,62 @@ class RSocketFactoryNettyTransportFragmentationTest { - @ParameterizedTest - @MethodSource("serverTransportProvider") - void serverErrorsWithEnabledFragmentationOnInsufficientMtu( - ServerTransport serverTransport) { - Mono server = createServer(serverTransport, f -> f.fragment(2)); - - StepVerifier.create(server) - .expectErrorMatches( - err -> - err instanceof IllegalArgumentException - && "smallest allowed mtu size is 64 bytes, provided: 2" - .equals(err.getMessage())) - .verify(Duration.ofSeconds(5)); + static Stream> arguments() { + return Stream.of(TcpServerTransport.create(0), WebsocketServerTransport.create(0)); } @ParameterizedTest - @MethodSource("serverTransportProvider") + @MethodSource("arguments") void serverSucceedsWithEnabledFragmentationOnSufficientMtu( ServerTransport serverTransport) { Mono server = - createServer(serverTransport, f -> f.fragment(100)).doOnNext(CloseableChannel::dispose); + RSocketServer.create(mockAcceptor()) + .fragment(100) + .bind(serverTransport) + .doOnNext(CloseableChannel::dispose); StepVerifier.create(server).expectNextCount(1).expectComplete().verify(Duration.ofSeconds(5)); } @ParameterizedTest - @MethodSource("serverTransportProvider") - void serverSucceedsWithDisabledFragmentation() { + @MethodSource("arguments") + void serverSucceedsWithDisabledFragmentation(ServerTransport serverTransport) { Mono server = - createServer(TcpServerTransport.create("localhost", 0), Function.identity()) + RSocketServer.create(mockAcceptor()) + .bind(serverTransport) .doOnNext(CloseableChannel::dispose); StepVerifier.create(server).expectNextCount(1).expectComplete().verify(Duration.ofSeconds(5)); } @ParameterizedTest - @MethodSource("serverTransportProvider") - void clientErrorsWithEnabledFragmentationOnInsufficientMtu( - ServerTransport serverTransport) { - CloseableChannel server = createServer(serverTransport, f -> f.fragment(100)).block(); - - Mono rSocket = - createClient(TcpClientTransport.create(server.address()), f -> f.fragment(2)) - .doFinally(s -> server.dispose()); - - StepVerifier.create(rSocket) - .expectErrorMatches( - err -> - err instanceof IllegalArgumentException - && "smallest allowed mtu size is 64 bytes, provided: 2" - .equals(err.getMessage())) - .verify(Duration.ofSeconds(5)); - } - - @ParameterizedTest - @MethodSource("serverTransportProvider") + @MethodSource("arguments") void clientSucceedsWithEnabledFragmentationOnSufficientMtu( ServerTransport serverTransport) { - CloseableChannel server = createServer(serverTransport, f -> f.fragment(100)).block(); + CloseableChannel server = + RSocketServer.create(mockAcceptor()).fragment(100).bind(serverTransport).block(); Mono rSocket = - createClient(TcpClientTransport.create(server.address()), f -> f.fragment(100)) + RSocketConnector.create() + .fragment(100) + .connect(TcpClientTransport.create(server.address())) .doFinally(s -> server.dispose()); StepVerifier.create(rSocket).expectNextCount(1).expectComplete().verify(Duration.ofSeconds(5)); } @ParameterizedTest - @MethodSource("serverTransportProvider") - void clientSucceedsWithDisabledFragmentation() { - CloseableChannel server = - createServer(TcpServerTransport.create("localhost", 0), Function.identity()).block(); + @MethodSource("arguments") + void clientSucceedsWithDisabledFragmentation(ServerTransport serverTransport) { + CloseableChannel server = RSocketServer.create(mockAcceptor()).bind(serverTransport).block(); Mono rSocket = - createClient(TcpClientTransport.create(server.address()), Function.identity()) + RSocketConnector.connectWith(TcpClientTransport.create(server.address())) .doFinally(s -> server.dispose()); StepVerifier.create(rSocket).expectNextCount(1).expectComplete().verify(Duration.ofSeconds(5)); } - private Mono createClient( - ClientTransport transport, Function f) { - return f.apply(RSocketFactory.connect()).transport(transport).start(); - } - - private Mono createServer( - ServerTransport transport, - Function f) { - return f.apply(receive()).acceptor(mockAcceptor()).transport(transport).start(); - } - private SocketAcceptor mockAcceptor() { SocketAcceptor mock = Mockito.mock(SocketAcceptor.class); Mockito.when(mock.accept(Mockito.any(), Mockito.any())) .thenReturn(Mono.just(Mockito.mock(RSocket.class))); return mock; } - - static Stream> serverTransportProvider() { - String host = "localhost"; - int port = 0; - return Stream.of( - TcpServerTransport.create(host, port), WebsocketServerTransport.create(host, port)); - } } diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SetupRejectionTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SetupRejectionTest.java index f32d28a0b..6fd3de791 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SetupRejectionTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/SetupRejectionTest.java @@ -2,8 +2,9 @@ import io.rsocket.ConnectionSetupPayload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; import io.rsocket.SocketAcceptor; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; @@ -41,19 +42,15 @@ void rejectSetupTcp( Mono serverRequester = acceptor.requesterRSocket(); CloseableChannel channel = - RSocketFactory.receive() - .acceptor(acceptor) - .transport(serverTransport.apply(new InetSocketAddress("localhost", 0))) - .start() + RSocketServer.create(acceptor) + .bind(serverTransport.apply(new InetSocketAddress("localhost", 0))) .block(Duration.ofSeconds(5)); ErrorConsumer errorConsumer = new ErrorConsumer(); RSocket clientRequester = - RSocketFactory.connect() - .errorConsumer(errorConsumer) - .transport(clientTransport.apply(channel.address())) - .start() + RSocketConnector.connectWith(clientTransport.apply(channel.address())) + .doOnError(errorConsumer) .block(Duration.ofSeconds(5)); StepVerifier.create(errorConsumer.errors().next()) diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPing.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPing.java index c2e136635..88c64648c 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPing.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPing.java @@ -17,7 +17,8 @@ package io.rsocket.transport.netty; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.Resume; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.test.PerfTest; import io.rsocket.test.PingClient; @@ -81,16 +82,15 @@ private static PingClient newResumablePingClient() { } private static PingClient newPingClient(boolean isResumable) { - RSocketFactory.ClientRSocketFactory clientRSocketFactory = RSocketFactory.connect(); + RSocketConnector connector = RSocketConnector.create(); if (isResumable) { - clientRSocketFactory.resume(); + connector.resume(new Resume()); } Mono rSocket = - clientRSocketFactory - .frameDecoder(PayloadDecoder.ZERO_COPY) - .keepAlive(Duration.ofMinutes(1), Duration.ofMinutes(30), 3) - .transport(TcpClientTransport.create(port)) - .start(); + connector + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .keepAlive(Duration.ofMinutes(1), Duration.ofMinutes(30)) + .connect(TcpClientTransport.create(port)); return new PingClient(rSocket); } diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java index b40f35e51..338868470 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpPongServer.java @@ -16,7 +16,8 @@ package io.rsocket.transport.netty; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketServer; +import io.rsocket.core.Resume; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.test.PingHandler; import io.rsocket.transport.netty.server.TcpServerTransport; @@ -31,15 +32,13 @@ public static void main(String... args) { System.out.println("port: " + port); System.out.println("resume enabled: " + isResume); - RSocketFactory.ServerRSocketFactory serverRSocketFactory = RSocketFactory.receive(); + RSocketServer server = RSocketServer.create(new PingHandler()); if (isResume) { - serverRSocketFactory.resume(); + server.resume(new Resume()); } - serverRSocketFactory - .frameDecoder(PayloadDecoder.ZERO_COPY) - .acceptor(new PingHandler()) - .transport(TcpServerTransport.create("localhost", port)) - .start() + server + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(TcpServerTransport.create("localhost", port)) .block() .onClose() .block(); diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java index 4fe40d232..7028a3846 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java @@ -3,7 +3,8 @@ import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.client.WebsocketClientTransport; import io.rsocket.transport.netty.server.WebsocketRouteTransport; @@ -23,8 +24,7 @@ public class WebSocketTransportIntegrationTest { @Test public void sendStreamOfDataWithExternalHttpServerTest() { ServerTransport.ConnectionAcceptor acceptor = - RSocketFactory.receive() - .acceptor( + RSocketServer.create( (setupPayload, sendingRSocket) -> { return Mono.just( new AbstractRSocket() { @@ -35,7 +35,7 @@ public Flux requestStream(Payload payload) { } }); }) - .toConnectionAcceptor(); + .asConnectionAcceptor(); DisposableServer server = HttpServer.create() @@ -44,11 +44,9 @@ public Flux requestStream(Payload payload) { .bindNow(); RSocket rsocket = - RSocketFactory.connect() - .transport( + RSocketConnector.connectWith( WebsocketClientTransport.create( URI.create("ws://" + server.host() + ":" + server.port() + "/test"))) - .start() .block(); StepVerifier.create(rsocket.requestStream(EmptyPayload.INSTANCE)) diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java index 306be4e43..a784a43c0 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPing.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -17,7 +17,7 @@ package io.rsocket.transport.netty; import io.rsocket.RSocket; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketConnector; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.test.PingClient; import io.rsocket.transport.netty.client.WebsocketClientTransport; @@ -29,10 +29,9 @@ public final class WebsocketPing { public static void main(String... args) { Mono client = - RSocketFactory.connect() - .frameDecoder(PayloadDecoder.ZERO_COPY) - .transport(WebsocketClientTransport.create(7878)) - .start(); + RSocketConnector.create() + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(WebsocketClientTransport.create(7878)); PingClient pingClient = new PingClient(client); diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java index eac091dd8..ab6c343de 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java @@ -8,7 +8,12 @@ import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.util.ReferenceCountUtil; -import io.rsocket.*; +import io.rsocket.AbstractRSocket; +import io.rsocket.Closeable; +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.core.RSocketConnector; +import io.rsocket.core.RSocketServer; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.client.WebsocketClientTransport; import io.rsocket.transport.netty.server.WebsocketRouteTransport; @@ -42,10 +47,8 @@ void tearDown() { @MethodSource("provideServerTransport") void webSocketPingPong(ServerTransport serverTransport) { server = - RSocketFactory.receive() - .acceptor((setup, sendingSocket) -> Mono.just(new EchoRSocket())) - .transport(serverTransport) - .start() + RSocketServer.create((setup, sendingSocket) -> Mono.just(new EchoRSocket())) + .bind(serverTransport) .block(); String expectedData = "data"; @@ -63,10 +66,7 @@ void webSocketPingPong(ServerTransport serverTransport) { .port(port)); RSocket rSocket = - RSocketFactory.connect() - .transport(WebsocketClientTransport.create(httpClient, "/")) - .start() - .block(); + RSocketConnector.connectWith(WebsocketClientTransport.create(httpClient, "/")).block(); rSocket .requestResponse(DefaultPayload.create(expectedData)) diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPongServer.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPongServer.java index 7fdb1813a..84dc816be 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPongServer.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPongServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,7 +16,7 @@ package io.rsocket.transport.netty; -import io.rsocket.RSocketFactory; +import io.rsocket.core.RSocketServer; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.test.PingHandler; import io.rsocket.transport.netty.server.WebsocketServerTransport; @@ -24,11 +24,9 @@ public final class WebsocketPongServer { public static void main(String... args) { - RSocketFactory.receive() - .frameDecoder(PayloadDecoder.ZERO_COPY) - .acceptor(new PingHandler()) - .transport(WebsocketServerTransport.create(7878)) - .start() + RSocketServer.create(new PingHandler()) + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(WebsocketServerTransport.create(7878)) .block() .onClose() .block(); From 5d3faef0d7899490db91d872cb797661e81b15de Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 15 Apr 2020 21:28:49 +0300 Subject: [PATCH 135/181] [Bugfix] Enforces reassembly to always be enabled (#775) * enforces reassembly on the receiver side Signed-off-by: Oleh Dokuka * optimizes FragmentationDuplexConnection by extending ReassemblyDuplexConnection Signed-off-by: Oleh Dokuka * fixes test Signed-off-by: Oleh Dokuka * fixes docs and headers Signed-off-by: Oleh Dokuka --- .../FragmentationDuplexConnection.java | 40 +-- .../fragmentation/FrameFragmenter.java | 2 +- .../fragmentation/FrameReassembler.java | 2 +- .../ReassemblyDuplexConnection.java | 89 ++++++ .../rsocket/fragmentation/package-info.java | 2 +- .../FragmentationDuplexConnectionTest.java | 221 +-------------- .../ReassembleDuplexConnectionTest.java | 266 ++++++++++++++++++ .../transport/ws/WebSocketHeadersSample.java | 7 +- .../transport/local/LocalClientTransport.java | 20 +- .../transport/local/LocalServerTransport.java | 4 + .../netty/client/TcpClientTransport.java | 4 +- .../client/WebsocketClientTransport.java | 4 + .../netty/server/TcpServerTransport.java | 5 +- .../netty/server/WebsocketRouteTransport.java | 3 + .../server/WebsocketServerTransport.java | 5 + .../io/rsocket/integration/FragmentTest.java | 40 ++- 16 files changed, 437 insertions(+), 277 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java create mode 100644 rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java index cbe989d4b..333787e9f 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -40,7 +40,8 @@ * href="https://github.com/rsocket/rsocket/blob/master/Protocol.md#fragmentation-and-reassembly">Fragmentation * and Reassembly */ -public final class FragmentationDuplexConnection implements DuplexConnection { +public final class FragmentationDuplexConnection extends ReassemblyDuplexConnection + implements DuplexConnection { private static final int MIN_MTU_SIZE = 64; private static final Logger logger = LoggerFactory.getLogger(FragmentationDuplexConnection.class); private final DuplexConnection delegate; @@ -54,11 +55,13 @@ public FragmentationDuplexConnection( DuplexConnection delegate, ByteBufAllocator allocator, int mtu, - boolean encodeLength, + boolean encodeAndEncodeLength, String type) { + super(delegate, allocator, encodeAndEncodeLength); + Objects.requireNonNull(delegate, "delegate must not be null"); Objects.requireNonNull(allocator, "byteBufAllocator must not be null"); - this.encodeLength = encodeLength; + this.encodeLength = encodeAndEncodeLength; this.allocator = allocator; this.delegate = delegate; this.mtu = assertMtu(mtu); @@ -137,33 +140,4 @@ private ByteBuf encode(ByteBuf frame) { return frame; } } - - private ByteBuf decode(ByteBuf frame) { - if (encodeLength) { - return FrameLengthFlyweight.frame(frame).retain(); - } else { - return frame; - } - } - - @Override - public Flux receive() { - return delegate - .receive() - .handle( - (byteBuf, sink) -> { - ByteBuf decode = decode(byteBuf); - frameReassembler.reassembleFrame(decode, sink); - }); - } - - @Override - public Mono onClose() { - return delegate.onClose(); - } - - @Override - public void dispose() { - delegate.dispose(); - } } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java index e59ece86f..8593d2be7 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java index 0c446a7c4..d8537ec1a 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java new file mode 100644 index 000000000..bf0d7482c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java @@ -0,0 +1,89 @@ +/* + * 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.fragmentation; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; +import io.rsocket.frame.FrameLengthFlyweight; +import java.util.Objects; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * A {@link DuplexConnection} implementation that reassembles {@link ByteBuf}s. + * + * @see Fragmentation + * and Reassembly + */ +public class ReassemblyDuplexConnection implements DuplexConnection { + private final DuplexConnection delegate; + private final FrameReassembler frameReassembler; + private final boolean decodeLength; + + public ReassemblyDuplexConnection( + DuplexConnection delegate, ByteBufAllocator allocator, boolean decodeLength) { + Objects.requireNonNull(delegate, "delegate must not be null"); + Objects.requireNonNull(allocator, "byteBufAllocator must not be null"); + this.decodeLength = decodeLength; + this.delegate = delegate; + this.frameReassembler = new FrameReassembler(allocator); + + delegate.onClose().doFinally(s -> frameReassembler.dispose()).subscribe(); + } + + @Override + public Mono send(Publisher frames) { + return delegate.send(frames); + } + + @Override + public Mono sendOne(ByteBuf frame) { + return delegate.sendOne(frame); + } + + private ByteBuf decode(ByteBuf frame) { + if (decodeLength) { + return FrameLengthFlyweight.frame(frame).retain(); + } else { + return frame; + } + } + + @Override + public Flux receive() { + return delegate + .receive() + .handle( + (byteBuf, sink) -> { + ByteBuf decode = decode(byteBuf); + frameReassembler.reassembleFrame(decode, sink); + }); + } + + @Override + public Mono onClose() { + return delegate.onClose(); + } + + @Override + public void dispose() { + delegate.dispose(); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/package-info.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/package-info.java index 4431f98dd..8cc3fb41a 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java index 3d96bfd12..a6918c497 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -22,18 +22,15 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.DuplexConnection; import io.rsocket.frame.*; -import io.rsocket.util.DefaultPayload; -import java.util.Arrays; -import java.util.List; import java.util.concurrent.ThreadLocalRandom; import org.junit.Assert; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -50,6 +47,10 @@ final class FragmentationDuplexConnectionTest { private final DuplexConnection delegate = mock(DuplexConnection.class, RETURNS_SMART_NULLS); + { + Mockito.when(delegate.onClose()).thenReturn(Mono.never()); + } + @SuppressWarnings("unchecked") private final ArgumentCaptor> publishers = ArgumentCaptor.forClass(Publisher.class); @@ -91,216 +92,6 @@ void constructorNullDelegate() { .withMessage("delegate must not be null"); } - @DisplayName("reassembles data") - @Test - void reassembleData() { - List byteBufs = - Arrays.asList( - RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)), - PayloadFrameFlyweight.encode( - allocator, 1, true, false, true, DefaultPayload.create(data)), - PayloadFrameFlyweight.encode( - allocator, 1, true, false, true, DefaultPayload.create(data)), - PayloadFrameFlyweight.encode( - allocator, 1, true, false, true, DefaultPayload.create(data)), - PayloadFrameFlyweight.encode( - allocator, 1, false, false, true, DefaultPayload.create(data))); - - CompositeByteBuf data = - allocator - .compositeDirectBuffer() - .addComponents( - true, - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data), - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data), - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data), - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data), - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data)); - - when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(delegate, allocator, 1030, false, "") - .receive() - .as(StepVerifier::create) - .assertNext( - byteBuf -> { - Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); - }) - .verifyComplete(); - } - - @DisplayName("reassembles metadata") - @Test - void reassembleMetadata() { - List byteBufs = - Arrays.asList( - RequestResponseFrameFlyweight.encode( - allocator, - 1, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), - PayloadFrameFlyweight.encode( - allocator, - 1, - true, - false, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), - PayloadFrameFlyweight.encode( - allocator, - 1, - true, - false, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), - PayloadFrameFlyweight.encode( - allocator, - 1, - true, - false, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), - PayloadFrameFlyweight.encode( - allocator, - 1, - false, - false, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata)))); - - CompositeByteBuf metadata = - allocator - .compositeDirectBuffer() - .addComponents( - true, - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata)); - - when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(delegate, allocator, 1030, false, "") - .receive() - .as(StepVerifier::create) - .assertNext( - byteBuf -> { - System.out.println(byteBuf.readableBytes()); - ByteBuf m = RequestResponseFrameFlyweight.metadata(byteBuf); - Assert.assertEquals(metadata, m); - }) - .verifyComplete(); - } - - @DisplayName("reassembles metadata and data") - @Test - void reassembleMetadataAndData() { - List byteBufs = - Arrays.asList( - RequestResponseFrameFlyweight.encode( - allocator, - 1, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), - PayloadFrameFlyweight.encode( - allocator, - 1, - true, - false, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), - PayloadFrameFlyweight.encode( - allocator, - 1, - true, - false, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), - PayloadFrameFlyweight.encode( - allocator, - 1, - true, - false, - true, - DefaultPayload.create( - Unpooled.wrappedBuffer(data), Unpooled.wrappedBuffer(metadata))), - PayloadFrameFlyweight.encode( - allocator, 1, false, false, true, DefaultPayload.create(data))); - - CompositeByteBuf data = - allocator - .compositeDirectBuffer() - .addComponents( - true, - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data), - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.data)); - - CompositeByteBuf metadata = - allocator - .compositeDirectBuffer() - .addComponents( - true, - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata), - Unpooled.wrappedBuffer(FragmentationDuplexConnectionTest.metadata)); - - when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(delegate, allocator, 1030, false, "") - .receive() - .as(StepVerifier::create) - .assertNext( - byteBuf -> { - Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); - Assert.assertEquals(metadata, RequestResponseFrameFlyweight.metadata(byteBuf)); - }) - .verifyComplete(); - } - - @DisplayName("does not reassemble a non-fragment frame") - @Test - void reassembleNonFragment() { - ByteBuf encode = - RequestResponseFrameFlyweight.encode( - allocator, 1, false, DefaultPayload.create(Unpooled.wrappedBuffer(data))); - - when(delegate.receive()).thenReturn(Flux.just(encode)); - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(delegate, allocator, 1030, false, "") - .receive() - .as(StepVerifier::create) - .assertNext( - byteBuf -> { - Assert.assertEquals( - Unpooled.wrappedBuffer(data), RequestResponseFrameFlyweight.data(byteBuf)); - }) - .verifyComplete(); - } - - @DisplayName("does not reassemble non fragmentable frame") - @Test - void reassembleNonFragmentableFrame() { - ByteBuf encode = CancelFrameFlyweight.encode(allocator, 2); - - when(delegate.receive()).thenReturn(Flux.just(encode)); - when(delegate.onClose()).thenReturn(Mono.never()); - - new FragmentationDuplexConnection(delegate, allocator, 1030, false, "") - .receive() - .as(StepVerifier::create) - .assertNext( - byteBuf -> { - Assert.assertEquals(FrameType.CANCEL, FrameHeaderFlyweight.frameType(byteBuf)); - }) - .verifyComplete(); - } - @DisplayName("fragments data") @Test void sendData() { diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java new file mode 100644 index 000000000..b21a7c9da --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java @@ -0,0 +1,266 @@ +/* + * 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.fragmentation; + +import static org.mockito.Mockito.RETURNS_SMART_NULLS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import io.netty.buffer.Unpooled; +import io.rsocket.DuplexConnection; +import io.rsocket.frame.CancelFrameFlyweight; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.PayloadFrameFlyweight; +import io.rsocket.frame.RequestResponseFrameFlyweight; +import io.rsocket.util.DefaultPayload; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import org.junit.Assert; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; + +final class ReassembleDuplexConnectionTest { + private static byte[] data = new byte[1024]; + private static byte[] metadata = new byte[1024]; + + static { + ThreadLocalRandom.current().nextBytes(data); + ThreadLocalRandom.current().nextBytes(metadata); + } + + private final DuplexConnection delegate = mock(DuplexConnection.class, RETURNS_SMART_NULLS); + + private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + + @DisplayName("reassembles data") + @Test + void reassembleData() { + List byteBufs = + Arrays.asList( + RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)), + PayloadFrameFlyweight.encode( + allocator, 1, true, false, true, DefaultPayload.create(data)), + PayloadFrameFlyweight.encode( + allocator, 1, true, false, true, DefaultPayload.create(data)), + PayloadFrameFlyweight.encode( + allocator, 1, true, false, true, DefaultPayload.create(data)), + PayloadFrameFlyweight.encode( + allocator, 1, false, false, true, DefaultPayload.create(data))); + + CompositeByteBuf data = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data)); + + when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); + when(delegate.onClose()).thenReturn(Mono.never()); + + new ReassemblyDuplexConnection(delegate, allocator, false) + .receive() + .as(StepVerifier::create) + .assertNext( + byteBuf -> { + Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); + }) + .verifyComplete(); + } + + @DisplayName("reassembles metadata") + @Test + void reassembleMetadata() { + List byteBufs = + Arrays.asList( + RequestResponseFrameFlyweight.encode( + allocator, + 1, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + false, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata)))); + + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata)); + + when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); + when(delegate.onClose()).thenReturn(Mono.never()); + + new ReassemblyDuplexConnection(delegate, allocator, false) + .receive() + .as(StepVerifier::create) + .assertNext( + byteBuf -> { + System.out.println(byteBuf.readableBytes()); + ByteBuf m = RequestResponseFrameFlyweight.metadata(byteBuf); + Assert.assertEquals(metadata, m); + }) + .verifyComplete(); + } + + @DisplayName("reassembles metadata and data") + @Test + void reassembleMetadataAndData() { + List byteBufs = + Arrays.asList( + RequestResponseFrameFlyweight.encode( + allocator, + 1, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, + 1, + true, + false, + true, + DefaultPayload.create( + Unpooled.wrappedBuffer(data), Unpooled.wrappedBuffer(metadata))), + PayloadFrameFlyweight.encode( + allocator, 1, false, false, true, DefaultPayload.create(data))); + + CompositeByteBuf data = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.data)); + + CompositeByteBuf metadata = + allocator + .compositeDirectBuffer() + .addComponents( + true, + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata), + Unpooled.wrappedBuffer(ReassembleDuplexConnectionTest.metadata)); + + when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); + when(delegate.onClose()).thenReturn(Mono.never()); + + new ReassemblyDuplexConnection(delegate, allocator, false) + .receive() + .as(StepVerifier::create) + .assertNext( + byteBuf -> { + Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); + Assert.assertEquals(metadata, RequestResponseFrameFlyweight.metadata(byteBuf)); + }) + .verifyComplete(); + } + + @DisplayName("does not reassemble a non-fragment frame") + @Test + void reassembleNonFragment() { + ByteBuf encode = + RequestResponseFrameFlyweight.encode( + allocator, 1, false, DefaultPayload.create(Unpooled.wrappedBuffer(data))); + + when(delegate.receive()).thenReturn(Flux.just(encode)); + when(delegate.onClose()).thenReturn(Mono.never()); + + new ReassemblyDuplexConnection(delegate, allocator, false) + .receive() + .as(StepVerifier::create) + .assertNext( + byteBuf -> { + Assert.assertEquals( + Unpooled.wrappedBuffer(data), RequestResponseFrameFlyweight.data(byteBuf)); + }) + .verifyComplete(); + } + + @DisplayName("does not reassemble non fragmentable frame") + @Test + void reassembleNonFragmentableFrame() { + ByteBuf encode = CancelFrameFlyweight.encode(allocator, 2); + + when(delegate.receive()).thenReturn(Flux.just(encode)); + when(delegate.onClose()).thenReturn(Mono.never()); + + new ReassemblyDuplexConnection(delegate, allocator, false) + .receive() + .as(StepVerifier::create) + .assertNext( + byteBuf -> { + Assert.assertEquals(FrameType.CANCEL, FrameHeaderFlyweight.frameType(byteBuf)); + }) + .verifyComplete(); + } +} 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 908505a2f..fd2dcbbd6 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 @@ -16,6 +16,7 @@ package io.rsocket.examples.transport.ws; +import io.netty.buffer.ByteBufAllocator; import io.netty.handler.codec.http.HttpResponseStatus; import io.rsocket.AbstractRSocket; import io.rsocket.ConnectionSetupPayload; @@ -25,6 +26,7 @@ import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; +import io.rsocket.fragmentation.ReassemblyDuplexConnection; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.WebsocketDuplexConnection; @@ -62,7 +64,10 @@ public static void main(String[] args) { (in, out) -> { if (in.headers().containsValue("Authorization", "test", true)) { DuplexConnection connection = - new WebsocketDuplexConnection((Connection) in); + new ReassemblyDuplexConnection( + new WebsocketDuplexConnection((Connection) in), + ByteBufAllocator.DEFAULT, + false); return acceptor.apply(connection).then(out.neverComplete()); } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java index 990acddfe..0d6a10391 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java @@ -20,6 +20,7 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; +import io.rsocket.fragmentation.ReassemblyDuplexConnection; import io.rsocket.internal.UnboundedProcessor; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; @@ -75,13 +76,16 @@ private Mono connect() { public Mono connect(int mtu) { Mono isError = FragmentationDuplexConnection.checkMtu(mtu); Mono connect = isError != null ? isError : connect(); - if (mtu > 0) { - return connect.map( - duplexConnection -> - new FragmentationDuplexConnection( - duplexConnection, ByteBufAllocator.DEFAULT, mtu, false, "client")); - } else { - return connect; - } + + return connect.map( + duplexConnection -> { + if (mtu > 0) { + return new FragmentationDuplexConnection( + duplexConnection, ByteBufAllocator.DEFAULT, mtu, false, "client"); + } else { + return new ReassemblyDuplexConnection( + duplexConnection, ByteBufAllocator.DEFAULT, false); + } + }); } } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java index d755859d2..329b4e38c 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java @@ -20,6 +20,7 @@ import io.rsocket.Closeable; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; +import io.rsocket.fragmentation.ReassemblyDuplexConnection; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import java.util.Objects; @@ -168,6 +169,9 @@ public void accept(DuplexConnection duplexConnection) { duplexConnection = new FragmentationDuplexConnection( duplexConnection, ByteBufAllocator.DEFAULT, mtu, false, "server"); + } else { + duplexConnection = + new ReassemblyDuplexConnection(duplexConnection, ByteBufAllocator.DEFAULT, false); } acceptor.apply(duplexConnection).subscribe(); diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java index f5e79e9bf..8059b36bd 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; +import io.rsocket.fragmentation.ReassemblyDuplexConnection; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.RSocketLengthCodec; @@ -110,7 +111,8 @@ public Mono connect(int mtu) { true, "client"); } else { - return new TcpDuplexConnection(c); + return new ReassemblyDuplexConnection( + new TcpDuplexConnection(c), ByteBufAllocator.DEFAULT, false); } }); } 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 62bbd9b99..49a2c2e92 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 @@ -23,6 +23,7 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; +import io.rsocket.fragmentation.ReassemblyDuplexConnection; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.TransportHeaderAware; @@ -165,6 +166,9 @@ public Mono connect(int mtu) { connection = new FragmentationDuplexConnection( connection, ByteBufAllocator.DEFAULT, mtu, false, "client"); + } else { + connection = + new ReassemblyDuplexConnection(connection, ByteBufAllocator.DEFAULT, false); } return connection; }); diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java index 54ef016c0..d39cc8e67 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; +import io.rsocket.fragmentation.ReassemblyDuplexConnection; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.RSocketLengthCodec; @@ -111,7 +112,9 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { true, "server"); } else { - connection = new TcpDuplexConnection(c); + connection = + new ReassemblyDuplexConnection( + new TcpDuplexConnection(c), ByteBufAllocator.DEFAULT, false); } acceptor .apply(connection) diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java index 60a34c9b1..1d8769cc6 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java @@ -22,6 +22,7 @@ import io.rsocket.Closeable; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; +import io.rsocket.fragmentation.ReassemblyDuplexConnection; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.netty.WebsocketDuplexConnection; import java.util.Objects; @@ -107,6 +108,8 @@ public static BiFunction> n connection = new FragmentationDuplexConnection( connection, ByteBufAllocator.DEFAULT, mtu, false, "server"); + } else { + connection = new ReassemblyDuplexConnection(connection, ByteBufAllocator.DEFAULT, false); } return acceptor.apply(connection).then(out.neverComplete()); }; diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java index 13caa6345..01c519ea3 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java @@ -21,6 +21,7 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; +import io.rsocket.fragmentation.ReassemblyDuplexConnection; import io.rsocket.transport.ClientTransport; import io.rsocket.transport.ServerTransport; import io.rsocket.transport.TransportHeaderAware; @@ -130,6 +131,10 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { connection = new FragmentationDuplexConnection( connection, ByteBufAllocator.DEFAULT, mtu, false, "server"); + } else { + connection = + new ReassemblyDuplexConnection( + connection, ByteBufAllocator.DEFAULT, false); } return acceptor.apply(connection).then(out.neverComplete()); }, diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java index db2f07bda..0ea938af2 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java @@ -29,22 +29,26 @@ import io.rsocket.util.DefaultPayload; import io.rsocket.util.RSocketProxy; import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Stream; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class FragmentTest { - private static final int frameSize = 64; private AbstractRSocket handler; private CloseableChannel server; private String message = null; private String metaData = null; private String responseMessage = null; - @BeforeEach - public void startup() { + private static Stream cases() { + return Stream.of(Arguments.of(0, 64), Arguments.of(64, 0), Arguments.of(64, 64)); + } + + public void startup(int frameSize) { int randomPort = ThreadLocalRandom.current().nextInt(10_000, 20_000); StringBuilder message = new StringBuilder(); StringBuilder responseMessage = new StringBuilder(); @@ -66,7 +70,7 @@ public void startup() { .block(); } - private RSocket buildClient() { + private RSocket buildClient(int frameSize) { return RSocketConnector.create() .fragment(frameSize) .connect(TcpClientTransport.create(server.address())) @@ -78,8 +82,10 @@ public void cleanup() { server.dispose(); } - @Test - void testFragmentNoMetaData() { + @ParameterizedTest + @MethodSource("cases") + void testFragmentNoMetaData(int clientFrameSize, int serverFrameSize) { + startup(serverFrameSize); System.out.println( "-------------------------------------------------testFragmentNoMetaData-------------------------------------------------"); handler = @@ -95,7 +101,7 @@ public Flux requestStream(Payload payload) { } }; - RSocket client = buildClient(); + RSocket client = buildClient(clientFrameSize); System.out.println("original message: " + message); System.out.println("original metadata: " + metaData); @@ -106,8 +112,10 @@ public Flux requestStream(Payload payload) { assertThat(responseMessage).isEqualTo(payload.getDataUtf8()); } - @Test - void testFragmentRequestMetaDataOnly() { + @ParameterizedTest + @MethodSource("cases") + void testFragmentRequestMetaDataOnly(int clientFrameSize, int serverFrameSize) { + startup(serverFrameSize); System.out.println( "-------------------------------------------------testFragmentRequestMetaDataOnly-------------------------------------------------"); handler = @@ -123,7 +131,7 @@ public Flux requestStream(Payload payload) { } }; - RSocket client = buildClient(); + RSocket client = buildClient(clientFrameSize); System.out.println("original message: " + message); System.out.println("original metadata: " + metaData); @@ -134,8 +142,10 @@ public Flux requestStream(Payload payload) { assertThat(responseMessage).isEqualTo(payload.getDataUtf8()); } - @Test - void testFragmentBothMetaData() { + @ParameterizedTest + @MethodSource("cases") + void testFragmentBothMetaData(int clientFrameSize, int serverFrameSize) { + startup(serverFrameSize); Payload responsePayload = DefaultPayload.create(responseMessage); System.out.println( "-------------------------------------------------testFragmentBothMetaData-------------------------------------------------"); @@ -162,7 +172,7 @@ public Mono requestResponse(Payload payload) { } }; - RSocket client = buildClient(); + RSocket client = buildClient(clientFrameSize); System.out.println("original message: " + message); System.out.println("original metadata: " + metaData); From c99c5ae709ba9bbc0c3d37f4c97773be4671ea93 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Fri, 17 Apr 2020 16:57:39 +0100 Subject: [PATCH 136/181] Follow-up fix for commit 0ac54d4b (#784) See gh-778 Signed-off-by: Rossen Stoyanchev --- rsocket-core/src/main/java/io/rsocket/RSocketFactory.java | 1 + .../src/main/java/io/rsocket/core/RSocketConnector.java | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index d488d2c08..7749d27cd 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -416,6 +416,7 @@ public ServerRSocketFactory addSocketAcceptorPlugin(SocketAcceptorInterceptor in } public ServerTransportAcceptor acceptor(SocketAcceptor acceptor) { + server.acceptor(acceptor); return this; } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 92c5220c0..23ecb9ba6 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -120,8 +120,8 @@ public RSocketConnector acceptor(SocketAcceptor acceptor) { * *

    By default {@code interval} is set to 20 seconds and {@code maxLifeTime} to 90 seconds. * - * @param interval the time between KEEPALIVE frames sent, must be > 0. - * @param maxLifeTime the max time allowed between KEEPALIVE frames received, must be > 0. + * @param interval the time between KEEPALIVE frames sent, must be greater than 0. + * @param maxLifeTime the max time between KEEPALIVE frames received, must be greater than 0. */ public RSocketConnector keepAlive(Duration interval, Duration maxLifeTime) { if (!interval.negated().isNegative()) { From cf3bf11eb328bee9828ba71d87d5bc98d667cfe9 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sat, 18 Apr 2020 09:03:54 +0100 Subject: [PATCH 137/181] Restore ConnectionSetupPayload as abstract class (#785) Another follow-up fix for commit 0ac54d4b. Changing ConnectionSetupPayload from abstract class to interface is problematic when framework code compiled against newer RSocket is used in an application on existing version. ConnectionSetupPayload is now an abstract class again but DefaultConnectionSetupPayload remains in the "core" sub-package to avoid the package cycle with "frame". See gh-778 Signed-off-by: Rossen Stoyanchev --- .../io/rsocket/ConnectionSetupPayload.java | 49 ++++++++++++--- .../core/DefaultConnectionSetupPayload.java | 61 ++++++++----------- 2 files changed, 64 insertions(+), 46 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java index 47cb0cf11..bd4582e2b 100644 --- a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java @@ -13,31 +13,60 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package io.rsocket; import io.netty.buffer.ByteBuf; -import io.netty.util.ReferenceCounted; +import io.netty.util.AbstractReferenceCounted; +import io.rsocket.core.DefaultConnectionSetupPayload; import javax.annotation.Nullable; /** * Exposes information from the {@code SETUP} frame to a server, as well as to client responders. */ -public interface ConnectionSetupPayload extends ReferenceCounted, Payload { +public abstract class ConnectionSetupPayload extends AbstractReferenceCounted implements Payload { - String metadataMimeType(); + public abstract String metadataMimeType(); - String dataMimeType(); + public abstract String dataMimeType(); - int keepAliveInterval(); + public abstract int keepAliveInterval(); - int keepAliveMaxLifetime(); + public abstract int keepAliveMaxLifetime(); - int getFlags(); + public abstract int getFlags(); - boolean willClientHonorLease(); + public abstract boolean willClientHonorLease(); - boolean isResumeEnabled(); + public abstract boolean isResumeEnabled(); @Nullable - ByteBuf resumeToken(); + public abstract ByteBuf resumeToken(); + + @Override + public ConnectionSetupPayload retain() { + super.retain(); + return this; + } + + @Override + public ConnectionSetupPayload retain(int increment) { + super.retain(increment); + return this; + } + + @Override + public abstract ConnectionSetupPayload touch(); + + /** + * Create a {@code ConnectionSetupPayload}. + * + * @deprecated as of 1.0 RC7. Please, use {@link + * DefaultConnectionSetupPayload#DefaultConnectionSetupPayload(ByteBuf) + * DefaultConnectionSetupPayload} constructor. + */ + @Deprecated + public static ConnectionSetupPayload create(final ByteBuf setupFrame) { + return new DefaultConnectionSetupPayload(setupFrame); + } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java index 23eeac160..8710aa61a 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java @@ -17,14 +17,15 @@ package io.rsocket.core; import io.netty.buffer.ByteBuf; -import io.netty.util.AbstractReferenceCounted; import io.rsocket.ConnectionSetupPayload; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.SetupFrameFlyweight; -/** Default implementation of {@link ConnectionSetupPayload}. */ -class DefaultConnectionSetupPayload extends AbstractReferenceCounted - implements ConnectionSetupPayload { +/** + * Default implementation of {@link ConnectionSetupPayload}. Primarily for internal use within + * RSocket Java but may be created in an application, e.g. for testing purposes. + */ +public class DefaultConnectionSetupPayload extends ConnectionSetupPayload { private final ByteBuf setupFrame; @@ -33,30 +34,28 @@ public DefaultConnectionSetupPayload(ByteBuf setupFrame) { } @Override - public ConnectionSetupPayload retain() { - super.retain(); - return this; + public boolean hasMetadata() { + return FrameHeaderFlyweight.hasMetadata(setupFrame); } @Override - public ConnectionSetupPayload retain(int increment) { - super.retain(increment); - return this; + public ByteBuf sliceMetadata() { + return SetupFrameFlyweight.metadata(setupFrame); } @Override - public boolean hasMetadata() { - return FrameHeaderFlyweight.hasMetadata(setupFrame); + public ByteBuf sliceData() { + return SetupFrameFlyweight.data(setupFrame); } @Override - public int keepAliveInterval() { - return SetupFrameFlyweight.keepAliveInterval(setupFrame); + public ByteBuf data() { + return sliceData(); } @Override - public int keepAliveMaxLifetime() { - return SetupFrameFlyweight.keepAliveMaxLifetime(setupFrame); + public ByteBuf metadata() { + return sliceMetadata(); } @Override @@ -69,6 +68,16 @@ public String dataMimeType() { return SetupFrameFlyweight.dataMimeType(setupFrame); } + @Override + public int keepAliveInterval() { + return SetupFrameFlyweight.keepAliveInterval(setupFrame); + } + + @Override + public int keepAliveMaxLifetime() { + return SetupFrameFlyweight.keepAliveMaxLifetime(setupFrame); + } + @Override public int getFlags() { return FrameHeaderFlyweight.flags(setupFrame); @@ -105,24 +114,4 @@ public ConnectionSetupPayload touch(Object hint) { protected void deallocate() { setupFrame.release(); } - - @Override - public ByteBuf sliceMetadata() { - return SetupFrameFlyweight.metadata(setupFrame); - } - - @Override - public ByteBuf sliceData() { - return SetupFrameFlyweight.data(setupFrame); - } - - @Override - public ByteBuf data() { - return sliceData(); - } - - @Override - public ByteBuf metadata() { - return sliceMetadata(); - } } From c05eb428bf79907aad872b1e8904bfe948334924 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Sun, 19 Apr 2020 09:25:30 +0100 Subject: [PATCH 138/181] RSocketFactory wrapping constructors (#788) Allow RSocketFactory to be created with a pre-created instance of RSocketConnector or RSocketServer. This helps higher level frameworks to support both old and current APIs. Signed-off-by: Rossen Stoyanchev --- .../main/java/io/rsocket/RSocketFactory.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 7749d27cd..0e1ae12cb 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -97,7 +97,7 @@ default Start transport(ServerTransport transport) { /** Factory to create and configure an RSocket client, and connect to a server. */ public static class ClientRSocketFactory implements ClientTransportAcceptor { - private final RSocketConnector connector = RSocketConnector.create(); + private final RSocketConnector connector; private Duration tickPeriod = Duration.ofSeconds(20); private Duration ackTimeout = Duration.ofSeconds(30); @@ -105,6 +105,14 @@ public static class ClientRSocketFactory implements ClientTransportAcceptor { private Resume resume; + public ClientRSocketFactory() { + this(RSocketConnector.create()); + } + + public ClientRSocketFactory(RSocketConnector connector) { + this.connector = connector; + } + public ClientRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { connector.byteBufAllocator(allocator); return this; @@ -375,10 +383,18 @@ public ClientRSocketFactory frameDecoder(PayloadDecoder payloadDecoder) { /** Factory to create, configure, and start an RSocket server. */ public static class ServerRSocketFactory implements ServerTransportAcceptor { - private final RSocketServer server = RSocketServer.create(); + private final RSocketServer server; private Resume resume; + public ServerRSocketFactory() { + this(RSocketServer.create()); + } + + public ServerRSocketFactory(RSocketServer server) { + this.server = server; + } + public ServerRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { server.byteBufAllocator(allocator); return this; From 070cffee5251fc2e9f7204f7b24cd82192c0d268 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 21 Apr 2020 23:12:05 +0300 Subject: [PATCH 139/181] Track Discarded Payloads (#777) * provides extra hooks to ensure we capture all discarded elements Signed-off-by: Oleh Dokuka * provides leaks tracking tooling Signed-off-by: Oleh Dokuka * provides leaks tracking tests and tooling Signed-off-by: Oleh Dokuka * more tests Signed-off-by: Oleh Dokuka * provides mechanism for terminates queue on calling clear Signed-off-by: Oleh Dokuka * provides workaround for FluxPublishOn to ensure that all elements are released in case of racing Signed-off-by: Oleh Dokuka * provides more tests part of the tests are on racing (ignored for now) another few on verification that elements are discarded properly Signed-off-by: Oleh Dokuka * provide fixes to RequestChannel responder and related tests. Ensures there is no leaks in RSocketRequesterTest and RSocketResponder tests Signed-off-by: Oleh Dokuka * tries to migrate to junit 5 Signed-off-by: Oleh Dokuka * fixes leaks in tests Signed-off-by: Oleh Dokuka * optimizes discarded/dropped BB consumption and releasing Signed-off-by: Oleh Dokuka * fixes javadocs Signed-off-by: Oleh Dokuka * removes hooks from Decoder Signed-off-by: Oleh Dokuka * fixes format Signed-off-by: Oleh Dokuka * rollbacks some fixes that should be delivered separately Signed-off-by: Oleh Dokuka * rollbacks some build.gradle refactoring Signed-off-by: Oleh Dokuka * fixes test Signed-off-by: Oleh Dokuka * fixes test Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/RSocketRequester.java | 25 +- .../io/rsocket/core/RSocketResponder.java | 22 +- .../java/io/rsocket/util/CharByteBufUtil.java | 7 +- .../buffer/LeaksTrackingByteBufAllocator.java | 167 ++++++ .../io/rsocket/core/AbstractSocketRule.java | 16 +- .../io/rsocket/core/RSocketRequesterTest.java | 386 +++++++++++-- .../io/rsocket/core/RSocketResponderTest.java | 521 ++++++++++++++++-- .../rsocket/frame/ByteBufRepresentation.java | 7 +- 8 files changed, 1049 insertions(+), 102 deletions(-) create mode 100644 rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index fc3175b15..70b2a1889 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -22,7 +22,9 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; import io.netty.util.collection.IntObjectMap; import io.rsocket.DuplexConnection; import io.rsocket.Payload; @@ -77,6 +79,16 @@ class RSocketRequester implements RSocket { AtomicReferenceFieldUpdater.newUpdater( RSocketRequester.class, Throwable.class, "terminationError"); private static final Exception CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); + private static final Consumer DROPPED_ELEMENTS_CONSUMER = + referenceCounted -> { + if (referenceCounted.refCnt() > 0) { + try { + referenceCounted.release(); + } catch (IllegalReferenceCountException e) { + // ignored + } + } + }; static { CLOSED_CHANNEL_EXCEPTION.setStackTrace(new StackTraceElement[0]); @@ -259,7 +271,7 @@ public void doOnTerminal( }); receivers.put(streamId, receiver); - return receiver; + return receiver.doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); } private Flux handleRequestStream(final Payload payload) { @@ -323,7 +335,8 @@ public void accept(long n) { sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); } }) - .doFinally(s -> removeStreamReceiver(streamId)); + .doFinally(s -> removeStreamReceiver(streamId)) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); } private Flux handleChannel(Flux request) { @@ -424,7 +437,10 @@ public void accept(long n) { senders.put(streamId, upstreamSubscriber); receivers.put(streamId, receiver); - inboundFlux.limitRate(Queues.SMALL_BUFFER_SIZE).subscribe(upstreamSubscriber); + inboundFlux + .limitRate(Queues.SMALL_BUFFER_SIZE) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER) + .subscribe(upstreamSubscriber); if (!payloadReleasedFlag.getAndSet(true)) { ByteBuf frame = RequestChannelFrameFlyweight.encode( @@ -461,7 +477,8 @@ public void accept(long n) { sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); upstreamSubscriber.cancel(); } - }); + }) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); } private Mono handleMetadataPush(Payload payload) { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index 6f235587a..e01000e49 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -20,7 +20,9 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; import io.netty.util.collection.IntObjectMap; import io.rsocket.DuplexConnection; import io.rsocket.Payload; @@ -45,6 +47,16 @@ /** Responder side of RSocket. Receives {@link ByteBuf}s from a peer's {@link RSocketRequester} */ class RSocketResponder implements ResponderRSocket { + private static final Consumer DROPPED_ELEMENTS_CONSUMER = + referenceCounted -> { + if (referenceCounted.refCnt() > 0) { + try { + referenceCounted.release(); + } catch (IllegalReferenceCountException e) { + // ignored + } + } + }; private final DuplexConnection connection; private final RSocket requestHandler; @@ -418,7 +430,7 @@ protected void hookFinally(SignalType type) { }; sendingSubscriptions.put(streamId, subscriber); - response.subscribe(subscriber); + response.doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER).subscribe(subscriber); } private void handleStream(int streamId, Flux response, int initialRequestN) { @@ -471,7 +483,10 @@ protected void hookFinally(SignalType type) { }; sendingSubscriptions.put(streamId, subscriber); - response.limitRate(Queues.SMALL_BUFFER_SIZE).subscribe(subscriber); + response + .limitRate(Queues.SMALL_BUFFER_SIZE) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER) + .subscribe(subscriber); } private void handleChannel(int streamId, Payload payload, int initialRequestN) { @@ -499,7 +514,8 @@ public void accept(long l) { sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); } }) - .doFinally(signalType -> channelProcessors.remove(streamId)); + .doFinally(signalType -> channelProcessors.remove(streamId)) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); // not chained, as the payload should be enqueued in the Unicast processor before this method // returns diff --git a/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java b/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java index 6f2aa7150..328fb8435 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/util/CharByteBufUtil.java @@ -99,7 +99,7 @@ private static char[] checkCharSequenceBounds(char[] seq, int start, int end) { } /** - * Encode a {@link char[]} in UTF-8 and write it + * Encode a {@code char[]} in UTF-8 and write it * into {@link ByteBuf}. * *

    This method returns the actual number of bytes written. @@ -109,9 +109,8 @@ public static int writeUtf8(ByteBuf buf, char[] seq) { } /** - * Equivalent to {@link #writeUtf8(ByteBuf, char[]) - * writeUtf8(buf, seq.subSequence(start, end), reserveBytes)} but avoids subsequence object - * allocation if possible. + * Equivalent to {@link #writeUtf8(ByteBuf, char[]) writeUtf8(buf, seq.subSequence(start, end), + * reserveBytes)} but avoids subsequence object allocation if possible. * * @return actual number of bytes written */ diff --git a/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java b/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java new file mode 100644 index 000000000..2044779ef --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java @@ -0,0 +1,167 @@ +package io.rsocket.buffer; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.CompositeByteBuf; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; +import org.assertj.core.api.Assertions; + +/** + * Additional Utils which allows to decorate a ByteBufAllocator and track/assertOnLeaks all created + * ByteBuffs + */ +public class LeaksTrackingByteBufAllocator implements ByteBufAllocator { + + /** + * Allows to instrument any given the instance of ByteBufAllocator + * + * @param allocator + * @return + */ + public static LeaksTrackingByteBufAllocator instrument(ByteBufAllocator allocator) { + return new LeaksTrackingByteBufAllocator(allocator); + } + + final ConcurrentLinkedQueue tracker = new ConcurrentLinkedQueue<>(); + + final ByteBufAllocator delegate; + + private LeaksTrackingByteBufAllocator(ByteBufAllocator delegate) { + this.delegate = delegate; + } + + public LeaksTrackingByteBufAllocator assertHasNoLeaks() { + try { + Assertions.assertThat(tracker) + .allSatisfy( + buf -> { + if (buf instanceof CompositeByteBuf) { + if (buf.refCnt() > 0) { + List decomposed = + ((CompositeByteBuf) buf).decompose(0, buf.readableBytes()); + for (int i = 0; i < decomposed.size(); i++) { + Assertions.assertThat(decomposed.get(i)) + .matches(bb -> bb.refCnt() == 0, "Got unreleased CompositeByteBuf"); + } + } + + } else { + Assertions.assertThat(buf) + .matches(bb -> bb.refCnt() == 0, "buffer should be released"); + } + }); + } finally { + tracker.clear(); + } + return this; + } + + // Delegating logic with tracking of buffers + + @Override + public ByteBuf buffer() { + return track(delegate.buffer()); + } + + @Override + public ByteBuf buffer(int initialCapacity) { + return track(delegate.buffer(initialCapacity)); + } + + @Override + public ByteBuf buffer(int initialCapacity, int maxCapacity) { + return track(delegate.buffer(initialCapacity, maxCapacity)); + } + + @Override + public ByteBuf ioBuffer() { + return track(delegate.ioBuffer()); + } + + @Override + public ByteBuf ioBuffer(int initialCapacity) { + return track(delegate.ioBuffer(initialCapacity)); + } + + @Override + public ByteBuf ioBuffer(int initialCapacity, int maxCapacity) { + return track(delegate.ioBuffer(initialCapacity, maxCapacity)); + } + + @Override + public ByteBuf heapBuffer() { + return track(delegate.heapBuffer()); + } + + @Override + public ByteBuf heapBuffer(int initialCapacity) { + return track(delegate.heapBuffer(initialCapacity)); + } + + @Override + public ByteBuf heapBuffer(int initialCapacity, int maxCapacity) { + return track(delegate.heapBuffer(initialCapacity, maxCapacity)); + } + + @Override + public ByteBuf directBuffer() { + return track(delegate.directBuffer()); + } + + @Override + public ByteBuf directBuffer(int initialCapacity) { + return track(delegate.directBuffer(initialCapacity)); + } + + @Override + public ByteBuf directBuffer(int initialCapacity, int maxCapacity) { + return track(delegate.directBuffer(initialCapacity, maxCapacity)); + } + + @Override + public CompositeByteBuf compositeBuffer() { + return track(delegate.compositeBuffer()); + } + + @Override + public CompositeByteBuf compositeBuffer(int maxNumComponents) { + return track(delegate.compositeBuffer(maxNumComponents)); + } + + @Override + public CompositeByteBuf compositeHeapBuffer() { + return track(delegate.compositeHeapBuffer()); + } + + @Override + public CompositeByteBuf compositeHeapBuffer(int maxNumComponents) { + return track(delegate.compositeHeapBuffer(maxNumComponents)); + } + + @Override + public CompositeByteBuf compositeDirectBuffer() { + return track(delegate.compositeDirectBuffer()); + } + + @Override + public CompositeByteBuf compositeDirectBuffer(int maxNumComponents) { + return track(delegate.compositeDirectBuffer(maxNumComponents)); + } + + @Override + public boolean isDirectBufferPooled() { + return delegate.isDirectBufferPooled(); + } + + @Override + public int calculateNewCapacity(int minNewCapacity, int maxCapacity) { + return delegate.calculateNewCapacity(minNewCapacity, maxCapacity); + } + + T track(T buffer) { + tracker.offer(buffer); + + return buffer; + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java index dc01e7911..5a43838c7 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java +++ b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java @@ -16,7 +16,9 @@ package io.rsocket.core; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.RSocket; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestSubscriber; import java.util.concurrent.ConcurrentLinkedQueue; @@ -32,6 +34,7 @@ public abstract class AbstractSocketRule extends ExternalReso protected Subscriber connectSub; protected T socket; protected ConcurrentLinkedQueue errors; + protected LeaksTrackingByteBufAllocator allocator; @Override public Statement apply(final Statement base, Description description) { @@ -41,6 +44,7 @@ public void evaluate() throws Throwable { connection = new TestDuplexConnection(); connectSub = TestSubscriber.create(); errors = new ConcurrentLinkedQueue<>(); + allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); init(); base.evaluate(); } @@ -48,14 +52,22 @@ public void evaluate() throws Throwable { } protected void init() { - socket = newRSocket(); + socket = newRSocket(allocator); } - protected abstract T newRSocket(); + protected abstract T newRSocket(LeaksTrackingByteBufAllocator allocator); public void assertNoConnectionErrors() { if (errors.size() > 1) { Assert.fail("No connection errors expected: " + errors.peek().toString()); } } + + public ByteBufAllocator alloc() { + return allocator; + } + + public void assertHasNoLeaks() { + allocator.assertHasNoLeaks(); + } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index 101500da7..586c9cfd3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -19,7 +19,6 @@ import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; import static io.rsocket.frame.FrameHeaderFlyweight.frameType; import static io.rsocket.frame.FrameType.CANCEL; -import static io.rsocket.frame.FrameType.KEEPALIVE; import static io.rsocket.frame.FrameType.REQUEST_CHANNEL; import static io.rsocket.frame.FrameType.REQUEST_RESPONSE; import static io.rsocket.frame.FrameType.REQUEST_STREAM; @@ -37,9 +36,12 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCounted; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ApplicationErrorException; +import io.rsocket.exceptions.CustomRSocketException; import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.CancelFrameFlyweight; import io.rsocket.frame.ErrorFrameFlyweight; @@ -50,8 +52,11 @@ import io.rsocket.frame.RequestChannelFrameFlyweight; import io.rsocket.frame.RequestNFrameFlyweight; import io.rsocket.frame.RequestStreamFrameFlyweight; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.subscriber.AssertSubscriber; import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.test.util.TestSubscriber; +import io.rsocket.util.ByteBufPayload; import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; import io.rsocket.util.MultiSubscriberRSocket; @@ -60,12 +65,17 @@ import java.util.Iterator; import java.util.List; import java.util.concurrent.ThreadLocalRandom; +import java.util.function.BiConsumer; import java.util.function.BiFunction; -import java.util.stream.Collectors; +import java.util.function.Function; import java.util.stream.Stream; import org.assertj.core.api.Assertions; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.runners.model.Statement; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -75,23 +85,39 @@ import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.UnicastProcessor; import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; +import reactor.test.util.RaceTestUtils; public class RSocketRequesterTest { - @Rule public final ClientSocketRule rule = new ClientSocketRule(); + ClientSocketRule rule; + + @BeforeEach + public void setUp() throws Throwable { + rule = new ClientSocketRule(); + rule.apply( + new Statement() { + @Override + public void evaluate() {} + }, + null) + .evaluate(); + } - @Test(timeout = 2_000) + @Test + @Timeout(2_000) public void testInvalidFrameOnStream0() { - rule.connection.addToReceivedBuffer( - RequestNFrameFlyweight.encode(ByteBufAllocator.DEFAULT, 0, 10)); + rule.connection.addToReceivedBuffer(RequestNFrameFlyweight.encode(rule.alloc(), 0, 10)); assertThat("Unexpected errors.", rule.errors, hasSize(1)); assertThat( "Unexpected error received.", rule.errors, contains(instanceOf(IllegalStateException.class))); + rule.assertHasNoLeaks(); } - @Test(timeout = 2_000) + @Test + @Timeout(2_000) public void testStreamInitialN() { Flux stream = rule.socket.requestStream(EmptyPayload.INSTANCE); @@ -100,19 +126,15 @@ public void testStreamInitialN() { @Override protected void hookOnSubscribe(Subscription subscription) { // don't request here - // subscription.request(3); } }; stream.subscribe(subscriber); + Assertions.assertThat(rule.connection.getSent()).isEmpty(); + subscriber.request(5); - List sent = - rule.connection - .getSent() - .stream() - .filter(f -> frameType(f) != KEEPALIVE) - .collect(Collectors.toList()); + List sent = new ArrayList<>(rule.connection.getSent()); assertThat("sent frame count", sent.size(), is(1)); @@ -120,21 +142,25 @@ protected void hookOnSubscribe(Subscription subscription) { assertThat("initial frame", frameType(f), is(REQUEST_STREAM)); assertThat("initial request n", RequestStreamFrameFlyweight.initialRequestN(f), is(5)); + assertThat("should be released", f.release(), is(true)); + rule.assertHasNoLeaks(); } - @Test(timeout = 2_000) + @Test + @Timeout(2_000) public void testHandleSetupException() { rule.connection.addToReceivedBuffer( - ErrorFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, 0, new RejectedSetupException("boom"))); + ErrorFrameFlyweight.encode(rule.alloc(), 0, new RejectedSetupException("boom"))); assertThat("Unexpected errors.", rule.errors, hasSize(1)); assertThat( "Unexpected error received.", rule.errors, contains(instanceOf(RejectedSetupException.class))); + rule.assertHasNoLeaks(); } - @Test(timeout = 2_000) + @Test + @Timeout(2_000) public void testHandleApplicationException() { rule.connection.clearSendReceiveBuffers(); Publisher response = rule.socket.requestResponse(EmptyPayload.INSTANCE); @@ -143,13 +169,20 @@ public void testHandleApplicationException() { int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); rule.connection.addToReceivedBuffer( - ErrorFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, streamId, new ApplicationErrorException("error"))); + ErrorFrameFlyweight.encode(rule.alloc(), streamId, new ApplicationErrorException("error"))); verify(responseSub).onError(any(ApplicationErrorException.class)); + + Assertions.assertThat(rule.connection.getSent()) + // requestResponseFrame FIXME + // .hasSize(1) + .allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); } - @Test(timeout = 2_000) + @Test + @Timeout(2_000) public void testHandleValidFrame() { Publisher response = rule.socket.requestResponse(EmptyPayload.INSTANCE); Subscriber sub = TestSubscriber.create(); @@ -157,13 +190,15 @@ public void testHandleValidFrame() { int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); rule.connection.addToReceivedBuffer( - PayloadFrameFlyweight.encodeNext( - ByteBufAllocator.DEFAULT, streamId, EmptyPayload.INSTANCE)); + PayloadFrameFlyweight.encodeNext(rule.alloc(), streamId, EmptyPayload.INSTANCE)); verify(sub).onComplete(); + Assertions.assertThat(rule.connection.getSent()).hasSize(1).allMatch(ReferenceCounted::release); + rule.assertHasNoLeaks(); } - @Test(timeout = 2_000) + @Test + @Timeout(2_000) public void testRequestReplyWithCancel() { Mono response = rule.socket.requestResponse(EmptyPayload.INSTANCE); @@ -172,19 +207,18 @@ public void testRequestReplyWithCancel() { } catch (IllegalStateException ise) { } - List sent = - rule.connection - .getSent() - .stream() - .filter(f -> frameType(f) != KEEPALIVE) - .collect(Collectors.toList()); + List sent = new ArrayList<>(rule.connection.getSent()); assertThat( "Unexpected frame sent on the connection.", frameType(sent.get(0)), is(REQUEST_RESPONSE)); assertThat("Unexpected frame sent on the connection.", frameType(sent.get(1)), is(CANCEL)); + Assertions.assertThat(sent).hasSize(2).allMatch(ReferenceCounted::release); + rule.assertHasNoLeaks(); } - @Test(timeout = 2_000) + @Test + @Disabled("invalid") + @Timeout(2_000) public void testRequestReplyErrorOnSend() { rule.connection.setAvailability(0); // Fails send Mono response = rule.socket.requestResponse(EmptyPayload.INSTANCE); @@ -195,21 +229,28 @@ public void testRequestReplyErrorOnSend() { verify(responseSub).onSubscribe(any(Subscription.class)); + rule.assertHasNoLeaks(); // TODO this should get the error reported through the response subscription // verify(responseSub).onError(any(RuntimeException.class)); } - @Test(timeout = 2_000) + @Test + @Timeout(2_000) public void testLazyRequestResponse() { Publisher response = new MultiSubscriberRSocket(rule.socket).requestResponse(EmptyPayload.INSTANCE); int streamId = sendRequestResponse(response); + Assertions.assertThat(rule.connection.getSent()).hasSize(1).allMatch(ReferenceCounted::release); + rule.assertHasNoLeaks(); rule.connection.clearSendReceiveBuffers(); int streamId2 = sendRequestResponse(response); assertThat("Stream ID reused.", streamId2, not(equalTo(streamId))); + Assertions.assertThat(rule.connection.getSent()).hasSize(1).allMatch(ReferenceCounted::release); + rule.assertHasNoLeaks(); } @Test + @Timeout(2_000) public void testChannelRequestCancellation() { MonoProcessor cancelled = MonoProcessor.create(); Flux request = Flux.never().doOnCancel(cancelled::onComplete); @@ -219,9 +260,11 @@ public void testChannelRequestCancellation() { Flux.error(new IllegalStateException("Channel request not cancelled")) .delaySubscription(Duration.ofSeconds(1))) .blockFirst(); + rule.assertHasNoLeaks(); } @Test + @Timeout(2_000) public void testChannelRequestCancellation2() { MonoProcessor cancelled = MonoProcessor.create(); Flux request = @@ -232,6 +275,8 @@ public void testChannelRequestCancellation2() { Flux.error(new IllegalStateException("Channel request not cancelled")) .delaySubscription(Duration.ofSeconds(1))) .blockFirst(); + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + rule.assertHasNoLeaks(); } @Test @@ -241,10 +286,9 @@ public void testChannelRequestServerSideCancellation() { request.onNext(EmptyPayload.INSTANCE); rule.socket.requestChannel(request).subscribe(cancelled); int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); + rule.connection.addToReceivedBuffer(CancelFrameFlyweight.encode(rule.alloc(), streamId)); rule.connection.addToReceivedBuffer( - CancelFrameFlyweight.encode(ByteBufAllocator.DEFAULT, streamId)); - rule.connection.addToReceivedBuffer( - PayloadFrameFlyweight.encodeComplete(ByteBufAllocator.DEFAULT, streamId)); + PayloadFrameFlyweight.encodeComplete(rule.alloc(), streamId)); Flux.first( cancelled, Flux.error(new IllegalStateException("Channel request not cancelled")) @@ -252,6 +296,12 @@ public void testChannelRequestServerSideCancellation() { .blockFirst(); Assertions.assertThat(request.isDisposed()).isTrue(); + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> frameType(bb) == REQUEST_CHANNEL) + .matches(ReferenceCounted::release); + rule.assertHasNoLeaks(); } @Test @@ -282,8 +332,10 @@ protected void hookOnSubscribe(Subscription subscription) {} Assertions.assertThat( RequestChannelFrameFlyweight.data(initialFrame).toString(CharsetUtil.UTF_8)) .isEqualTo("0"); + Assertions.assertThat(initialFrame.release()).isTrue(); Assertions.assertThat(iterator.hasNext()).isFalse(); + rule.assertHasNoLeaks(); } @Test @@ -304,9 +356,21 @@ public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmen .isInstanceOf(IllegalArgumentException.class) .hasMessage(INVALID_PAYLOAD_ERROR_MESSAGE)) .verify(); + // FIXME: should be removed + Assertions.assertThat(rule.connection.getSent()).allMatch(bb -> bb.release()); + rule.assertHasNoLeaks(); }); } + static Stream>> prepareCalls() { + return Stream.of( + RSocket::fireAndForget, + RSocket::requestResponse, + RSocket::requestStream, + (rSocket, payload) -> rSocket.requestChannel(Flux.just(payload)), + RSocket::metadataPush); + } + @Test public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmentationForRequestChannelCase() { @@ -322,24 +386,245 @@ public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmen () -> rule.connection.addToReceivedBuffer( RequestNFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, - rule.getStreamIdForRequestType(REQUEST_CHANNEL), - 2))) + rule.alloc(), rule.getStreamIdForRequestType(REQUEST_CHANNEL), 2))) .expectErrorSatisfies( t -> Assertions.assertThat(t) .isInstanceOf(IllegalArgumentException.class) .hasMessage(INVALID_PAYLOAD_ERROR_MESSAGE)) .verify(); + Assertions.assertThat(rule.connection.getSent()) + // expect to be sent RequestChannelFrame + // expect to be sent CancelFrame + .hasSize(2) + .allMatch(ReferenceCounted::release); + rule.assertHasNoLeaks(); } - static Stream>> prepareCalls() { + @Test + @Disabled("Due to https://github.com/reactor/reactor-core/pull/2114") + @SuppressWarnings("unchecked") + public void checkNoLeaksOnRacingTest() { + + racingCases() + .forEach( + a -> { + ((Runnable) a.get()[0]).run(); + checkNoLeaksOnRacing( + (Function>) a.get()[1], + (BiConsumer, ClientSocketRule>) a.get()[2]); + }); + } + + public void checkNoLeaksOnRacing( + Function> initiator, + BiConsumer, ClientSocketRule> runner) { + for (int i = 0; i < 10000; i++) { + ClientSocketRule clientSocketRule = new ClientSocketRule(); + try { + clientSocketRule + .apply( + new Statement() { + @Override + public void evaluate() {} + }, + null) + .evaluate(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + + Publisher payloadP = initiator.apply(clientSocketRule); + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + + if (payloadP instanceof Flux) { + ((Flux) payloadP).doOnNext(Payload::release).subscribe(assertSubscriber); + } else { + ((Mono) payloadP).doOnNext(Payload::release).subscribe(assertSubscriber); + } + + runner.accept(assertSubscriber, clientSocketRule); + + Assertions.assertThat(clientSocketRule.connection.getSent()) + .allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + } + + private static Stream racingCases() { return Stream.of( - RSocket::fireAndForget, - RSocket::requestResponse, - RSocket::requestStream, - (rSocket, payload) -> rSocket.requestChannel(Flux.just(payload)), - RSocket::metadataPush); + Arguments.of( + (Runnable) () -> System.out.println("RequestStream downstream cancellation case"), + (Function>) + (rule) -> rule.socket.requestStream(EmptyPayload.INSTANCE), + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + ByteBufAllocator allocator = rule.alloc(); + ByteBuf metadata = allocator.buffer(); + metadata.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data = allocator.buffer(); + data.writeCharSequence("def", CharsetUtil.UTF_8); + int streamId = rule.getStreamIdForRequestType(REQUEST_STREAM); + ByteBuf frame = + PayloadFrameFlyweight.encode( + allocator, streamId, false, false, true, metadata, data); + + RaceTestUtils.race(as::cancel, () -> rule.connection.addToReceivedBuffer(frame)); + }), + Arguments.of( + (Runnable) () -> System.out.println("RequestChannel downstream cancellation case"), + (Function>) + (rule) -> rule.socket.requestChannel(Flux.just(EmptyPayload.INSTANCE)), + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + ByteBufAllocator allocator = rule.alloc(); + ByteBuf metadata = allocator.buffer(); + metadata.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data = allocator.buffer(); + data.writeCharSequence("def", CharsetUtil.UTF_8); + int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); + ByteBuf frame = + PayloadFrameFlyweight.encode( + allocator, streamId, false, false, true, metadata, data); + + RaceTestUtils.race(as::cancel, () -> rule.connection.addToReceivedBuffer(frame)); + }), + Arguments.of( + (Runnable) () -> System.out.println("RequestChannel upstream cancellation 1"), + (Function>) + (rule) -> { + ByteBufAllocator allocator = rule.alloc(); + ByteBuf metadata = allocator.buffer(); + metadata.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data = allocator.buffer(); + data.writeCharSequence("def", CharsetUtil.UTF_8); + return rule.socket.requestChannel( + Flux.just(ByteBufPayload.create(data, metadata))); + }, + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + ByteBufAllocator allocator = rule.alloc(); + int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); + ByteBuf frame = CancelFrameFlyweight.encode(allocator, streamId); + + RaceTestUtils.race( + () -> as.request(1), () -> rule.connection.addToReceivedBuffer(frame)); + }), + Arguments.of( + (Runnable) () -> System.out.println("RequestChannel upstream cancellation 2"), + (Function>) + (rule) -> + rule.socket.requestChannel( + Flux.generate( + () -> 1L, + (index, sink) -> { + final Payload payload = + ByteBufPayload.create("d" + index, "m" + index); + sink.next(payload); + return ++index; + })), + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + ByteBufAllocator allocator = rule.alloc(); + int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); + ByteBuf frame = CancelFrameFlyweight.encode(allocator, streamId); + + as.request(1); + + RaceTestUtils.race( + () -> as.request(Long.MAX_VALUE), + () -> rule.connection.addToReceivedBuffer(frame)); + }), + Arguments.of( + (Runnable) () -> System.out.println("RequestChannel remote error"), + (Function>) + (rule) -> + rule.socket.requestChannel( + Flux.generate( + () -> 1L, + (index, sink) -> { + final Payload payload = + ByteBufPayload.create("d" + index, "m" + index); + sink.next(payload); + return ++index; + })), + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + ByteBufAllocator allocator = rule.alloc(); + int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); + ByteBuf frame = + ErrorFrameFlyweight.encode(allocator, streamId, new RuntimeException("test")); + + as.request(1); + + RaceTestUtils.race( + () -> as.request(Long.MAX_VALUE), + () -> rule.connection.addToReceivedBuffer(frame)); + }), + Arguments.of( + (Runnable) () -> System.out.println("RequestResponse downstream cancellation"), + (Function>) + (rule) -> rule.socket.requestResponse(EmptyPayload.INSTANCE), + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + ByteBufAllocator allocator = rule.alloc(); + ByteBuf metadata = allocator.buffer(); + metadata.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data = allocator.buffer(); + data.writeCharSequence("def", CharsetUtil.UTF_8); + int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); + ByteBuf frame = + PayloadFrameFlyweight.encode( + allocator, streamId, false, false, true, metadata, data); + + RaceTestUtils.race(as::cancel, () -> rule.connection.addToReceivedBuffer(frame)); + })); + } + + @Test + public void simpleOnDiscardRequestChannelTest() { + AssertSubscriber assertSubscriber = AssertSubscriber.create(1); + TestPublisher testPublisher = TestPublisher.create(); + + Flux payloadFlux = rule.socket.requestChannel(testPublisher); + + payloadFlux.subscribe(assertSubscriber); + + testPublisher.next( + ByteBufPayload.create("d", "m"), + ByteBufPayload.create("d1", "m1"), + ByteBufPayload.create("d2", "m2")); + + assertSubscriber.cancel(); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); + + rule.assertHasNoLeaks(); + } + + @Test + public void simpleOnDiscardRequestChannelTest2() { + ByteBufAllocator allocator = rule.alloc(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(1); + TestPublisher testPublisher = TestPublisher.create(); + + Flux payloadFlux = rule.socket.requestChannel(testPublisher); + + payloadFlux.subscribe(assertSubscriber); + + testPublisher.next(ByteBufPayload.create("d", "m")); + + int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); + testPublisher.next(ByteBufPayload.create("d1", "m1"), ByteBufPayload.create("d2", "m2")); + + rule.connection.addToReceivedBuffer( + ErrorFrameFlyweight.encode( + allocator, streamId, new CustomRSocketException(0x00000404, "test"))); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); + + rule.assertHasNoLeaks(); } public int sendRequestResponse(Publisher response) { @@ -347,8 +632,7 @@ public int sendRequestResponse(Publisher response) { response.subscribe(sub); int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); rule.connection.addToReceivedBuffer( - PayloadFrameFlyweight.encodeNextComplete( - ByteBufAllocator.DEFAULT, streamId, EmptyPayload.INSTANCE)); + PayloadFrameFlyweight.encodeNextComplete(rule.alloc(), streamId, EmptyPayload.INSTANCE)); verify(sub).onNext(any(Payload.class)); verify(sub).onComplete(); return streamId; @@ -356,11 +640,11 @@ public int sendRequestResponse(Publisher response) { public static class ClientSocketRule extends AbstractSocketRule { @Override - protected RSocketRequester newRSocket() { + protected RSocketRequester newRSocket(LeaksTrackingByteBufAllocator allocator) { return new RSocketRequester( - ByteBufAllocator.DEFAULT, + allocator, connection, - DefaultPayload::create, + PayloadDecoder.ZERO_COPY, throwable -> errors.add(throwable), StreamIdSupplier.clientSupplier(), 0, diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index 5c147f46f..d31fc3bf7 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -18,43 +18,93 @@ import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; import static io.rsocket.frame.FrameHeaderFlyweight.frameType; +import static io.rsocket.frame.FrameType.REQUEST_CHANNEL; +import static io.rsocket.frame.FrameType.REQUEST_RESPONSE; +import static io.rsocket.frame.FrameType.REQUEST_STREAM; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.anyOf; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.frame.*; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import io.rsocket.frame.CancelFrameFlyweight; +import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.KeepAliveFrameFlyweight; +import io.rsocket.frame.PayloadFrameFlyweight; +import io.rsocket.frame.RequestChannelFrameFlyweight; +import io.rsocket.frame.RequestNFrameFlyweight; +import io.rsocket.frame.RequestResponseFrameFlyweight; +import io.rsocket.frame.RequestStreamFrameFlyweight; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.subscriber.AssertSubscriber; import io.rsocket.lease.ResponderLeaseHandler; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestSubscriber; +import io.rsocket.util.ByteBufPayload; import io.rsocket.util.DefaultPayload; -import io.rsocket.util.EmptyPayload; import java.util.Collection; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; import org.assertj.core.api.Assertions; -import org.junit.Ignore; -import org.junit.Rule; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.runners.model.Statement; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; +import reactor.core.CoreSubscriber; import reactor.core.publisher.Flux; +import reactor.core.publisher.FluxSink; +import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; +import reactor.core.publisher.Operators; +import reactor.core.scheduler.Scheduler; +import reactor.core.scheduler.Schedulers; +import reactor.test.util.RaceTestUtils; public class RSocketResponderTest { - @Rule public final ServerSocketRule rule = new ServerSocketRule(); + ServerSocketRule rule; - @Test(timeout = 2000) - @Ignore + @BeforeEach + public void setUp() throws Throwable { + rule = new ServerSocketRule(); + rule.apply( + new Statement() { + @Override + public void evaluate() {} + }, + null) + .evaluate(); + } + + @AfterEach + public void tearDown() { + Hooks.resetOnErrorDropped(); + } + + @Test + @Timeout(2_000) + @Disabled public void testHandleKeepAlive() throws Exception { rule.connection.addToReceivedBuffer( - KeepAliveFrameFlyweight.encode(ByteBufAllocator.DEFAULT, true, 0, Unpooled.EMPTY_BUFFER)); + KeepAliveFrameFlyweight.encode(rule.alloc(), true, 0, Unpooled.EMPTY_BUFFER)); ByteBuf sent = rule.connection.awaitSend(); assertThat("Unexpected frame sent.", frameType(sent), is(FrameType.KEEPALIVE)); /*Keep alive ack must not have respond flag else, it will result in infinite ping-pong of keep alive frames.*/ @@ -64,8 +114,9 @@ public void testHandleKeepAlive() throws Exception { is(false)); } - @Test(timeout = 2000) - @Ignore + @Test + @Timeout(2_000) + @Disabled public void testHandleResponseFrameNoError() throws Exception { final int streamId = 4; rule.connection.clearSendReceiveBuffers(); @@ -82,8 +133,9 @@ public void testHandleResponseFrameNoError() throws Exception { anyOf(is(FrameType.COMPLETE), is(FrameType.NEXT_COMPLETE))); } - @Test(timeout = 2000) - @Ignore + @Test + @Timeout(2_000) + @Disabled public void testHandlerEmitsError() throws Exception { final int streamId = 4; rule.sendRequest(streamId, FrameType.REQUEST_STREAM); @@ -92,14 +144,17 @@ public void testHandlerEmitsError() throws Exception { "Unexpected frame sent.", frameType(rule.connection.awaitSend()), is(FrameType.ERROR)); } - @Test(timeout = 2_0000) + @Test + @Timeout(20_000) public void testCancel() { + ByteBufAllocator allocator = rule.alloc(); final int streamId = 4; final AtomicBoolean cancelled = new AtomicBoolean(); rule.setAcceptingSocket( new AbstractRSocket() { @Override public Mono requestResponse(Payload payload) { + payload.release(); return Mono.never().doOnCancel(() -> cancelled.set(true)); } }); @@ -108,14 +163,15 @@ public Mono requestResponse(Payload payload) { assertThat("Unexpected error.", rule.errors, is(empty())); assertThat("Unexpected frame sent.", rule.connection.getSent(), is(empty())); - rule.connection.addToReceivedBuffer( - CancelFrameFlyweight.encode(ByteBufAllocator.DEFAULT, streamId)); + rule.connection.addToReceivedBuffer(CancelFrameFlyweight.encode(allocator, streamId)); assertThat("Unexpected frame sent.", rule.connection.getSent(), is(empty())); assertThat("Subscription not cancelled.", cancelled.get(), is(true)); + rule.assertHasNoLeaks(); } @Test + @Timeout(2_000) public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmentation() { final int streamId = 4; final AtomicBoolean cancelled = new AtomicBoolean(); @@ -128,48 +184,429 @@ public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmen new AbstractRSocket() { @Override public Mono requestResponse(Payload p) { + p.release(); return Mono.just(payload).doOnCancel(() -> cancelled.set(true)); } @Override public Flux requestStream(Payload p) { + p.release(); return Flux.just(payload).doOnCancel(() -> cancelled.set(true)); } - - @Override - public Flux requestChannel(Publisher payloads) { - return Flux.just(payload).doOnCancel(() -> cancelled.set(true)); - } + // FIXME + // @Override + // public Flux requestChannel(Publisher payloads) { + // Flux.from(payloads) + // .doOnNext(Payload::release) + // .subscribe( + // new BaseSubscriber() { + // @Override + // protected void hookOnSubscribe(Subscription subscription) { + // subscription.request(1); + // } + // }); + // return Flux.just(payload).doOnCancel(() -> cancelled.set(true)); + // } }; rule.setAcceptingSocket(acceptingSocket); final Runnable[] runnables = { () -> rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE), - () -> rule.sendRequest(streamId, FrameType.REQUEST_STREAM), - () -> rule.sendRequest(streamId, FrameType.REQUEST_CHANNEL) + () -> rule.sendRequest(streamId, FrameType.REQUEST_STREAM) /* FIXME, + () -> rule.sendRequest(streamId, FrameType.REQUEST_CHANNEL)*/ }; for (Runnable runnable : runnables) { + rule.connection.clearSendReceiveBuffers(); runnable.run(); Assertions.assertThat(rule.errors) .first() .isInstanceOf(IllegalArgumentException.class) .hasToString("java.lang.IllegalArgumentException: " + INVALID_PAYLOAD_ERROR_MESSAGE); Assertions.assertThat(rule.connection.getSent()) + .filteredOn(bb -> FrameHeaderFlyweight.frameType(bb) == FrameType.ERROR) .hasSize(1) .first() - .matches(bb -> FrameHeaderFlyweight.frameType(bb) == FrameType.ERROR) - .matches(bb -> ErrorFrameFlyweight.dataUtf8(bb).contains(INVALID_PAYLOAD_ERROR_MESSAGE)); + .matches(bb -> ErrorFrameFlyweight.dataUtf8(bb).contains(INVALID_PAYLOAD_ERROR_MESSAGE)) + .matches(ReferenceCounted::release); assertThat("Subscription not cancelled.", cancelled.get(), is(true)); - rule.init(); - rule.setAcceptingSocket(acceptingSocket); } + + rule.assertHasNoLeaks(); + } + + @Test + @Disabled("Due to https://github.com/reactor/reactor-core/pull/2114") + public void checkNoLeaksOnRacingCancelFromRequestChannelAndNextFromUpstream() { + + ByteBufAllocator allocator = rule.alloc(); + for (int i = 0; i < 10000; i++) { + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + + rule.setAcceptingSocket( + new AbstractRSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + ((Flux) payloads) + .doOnNext(ReferenceCountUtil::safeRelease) + .subscribe(assertSubscriber); + return Flux.never(); + } + }, + Integer.MAX_VALUE); + + rule.sendRequest(1, REQUEST_CHANNEL); + ByteBuf metadata1 = allocator.buffer(); + metadata1.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data1 = allocator.buffer(); + data1.writeCharSequence("def", CharsetUtil.UTF_8); + ByteBuf nextFrame1 = + PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata1, data1); + + ByteBuf metadata2 = allocator.buffer(); + metadata2.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data2 = allocator.buffer(); + data2.writeCharSequence("def", CharsetUtil.UTF_8); + ByteBuf nextFrame2 = + PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata2, data2); + + ByteBuf metadata3 = allocator.buffer(); + metadata3.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data3 = allocator.buffer(); + data3.writeCharSequence("def", CharsetUtil.UTF_8); + ByteBuf nextFrame3 = + PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata3, data3); + + RaceTestUtils.race( + () -> { + rule.connection.addToReceivedBuffer(nextFrame1, nextFrame2, nextFrame3); + }, + assertSubscriber::cancel); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + } + + @Test + @Disabled("Due to https://github.com/reactor/reactor-core/pull/2114") + public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChannelTest() { + Hooks.onErrorDropped((e) -> {}); + ByteBufAllocator allocator = rule.alloc(); + for (int i = 0; i < 10000; i++) { + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + + FluxSink[] sinks = new FluxSink[1]; + + rule.setAcceptingSocket( + new AbstractRSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + ((Flux) payloads) + .doOnNext(ReferenceCountUtil::safeRelease) + .subscribe(assertSubscriber); + return Flux.create(sink -> sinks[0] = sink, FluxSink.OverflowStrategy.IGNORE); + } + }, + 1); + + rule.sendRequest(1, REQUEST_CHANNEL); + + ByteBuf cancelFrame = CancelFrameFlyweight.encode(allocator, 1); + FluxSink sink = sinks[0]; + RaceTestUtils.race( + () -> rule.connection.addToReceivedBuffer(cancelFrame), + () -> { + sink.next(ByteBufPayload.create("d1", "m1")); + sink.next(ByteBufPayload.create("d2", "m2")); + sink.next(ByteBufPayload.create("d3", "m3")); + }); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + } + + @Test + @Disabled("Due to https://github.com/reactor/reactor-core/pull/2114") + public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChannelTest1() { + Scheduler parallel = Schedulers.parallel(); + Hooks.onErrorDropped((e) -> {}); + ByteBufAllocator allocator = rule.alloc(); + for (int i = 0; i < 10000; i++) { + AssertSubscriber assertSubscriber = AssertSubscriber.create(); + + FluxSink[] sinks = new FluxSink[1]; + + rule.setAcceptingSocket( + new AbstractRSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + ((Flux) payloads) + .doOnNext(ReferenceCountUtil::safeRelease) + .subscribe(assertSubscriber); + return Flux.create(sink -> sinks[0] = sink, FluxSink.OverflowStrategy.IGNORE); + } + }, + 1); + + rule.sendRequest(1, REQUEST_CHANNEL); + + ByteBuf cancelFrame = CancelFrameFlyweight.encode(allocator, 1); + ByteBuf requestNFrame = RequestNFrameFlyweight.encode(allocator, 1, Integer.MAX_VALUE); + FluxSink sink = sinks[0]; + RaceTestUtils.race( + () -> + RaceTestUtils.race( + () -> rule.connection.addToReceivedBuffer(requestNFrame), + () -> rule.connection.addToReceivedBuffer(cancelFrame), + parallel), + () -> { + sink.next(ByteBufPayload.create("d1", "m1")); + sink.next(ByteBufPayload.create("d2", "m2")); + sink.next(ByteBufPayload.create("d3", "m3")); + }, + parallel); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + } + + @Test + @Disabled("Due to https://github.com/reactor/reactor-core/pull/2114") + public void + checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromUpstreamOnErrorFromRequestChannelTest1() + throws InterruptedException { + Scheduler parallel = Schedulers.parallel(); + Hooks.onErrorDropped((e) -> {}); + ByteBufAllocator allocator = rule.alloc(); + for (int i = 0; i < 10000; i++) { + FluxSink[] sinks = new FluxSink[1]; + + rule.setAcceptingSocket( + new AbstractRSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + + return Flux.create( + sink -> { + sinks[0] = sink; + }, + FluxSink.OverflowStrategy.IGNORE) + .mergeWith(payloads); + } + }, + 1); + + rule.sendRequest(1, REQUEST_CHANNEL); + + ByteBuf metadata1 = allocator.buffer(); + metadata1.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data1 = allocator.buffer(); + data1.writeCharSequence("def", CharsetUtil.UTF_8); + ByteBuf nextFrame1 = + PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata1, data1); + + ByteBuf metadata2 = allocator.buffer(); + metadata2.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data2 = allocator.buffer(); + data2.writeCharSequence("def", CharsetUtil.UTF_8); + ByteBuf nextFrame2 = + PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata2, data2); + + ByteBuf metadata3 = allocator.buffer(); + metadata3.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data3 = allocator.buffer(); + data3.writeCharSequence("def", CharsetUtil.UTF_8); + ByteBuf nextFrame3 = + PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata3, data3); + + ByteBuf requestNFrame = RequestNFrameFlyweight.encode(allocator, 1, Integer.MAX_VALUE); + + FluxSink sink = sinks[0]; + RaceTestUtils.race( + () -> + RaceTestUtils.race( + () -> rule.connection.addToReceivedBuffer(requestNFrame), + () -> rule.connection.addToReceivedBuffer(nextFrame1, nextFrame2, nextFrame3), + parallel), + () -> { + sink.next(ByteBufPayload.create("d1", "m1")); + sink.next(ByteBufPayload.create("d2", "m2")); + sink.next(ByteBufPayload.create("d3", "m3")); + sink.error(new RuntimeException()); + }, + parallel); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + } + + @Test + @Disabled("Due to https://github.com/reactor/reactor-core/pull/2114") + public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestStreamTest1() { + Scheduler parallel = Schedulers.parallel(); + Hooks.onErrorDropped((e) -> {}); + ByteBufAllocator allocator = rule.alloc(); + for (int i = 0; i < 10000; i++) { + FluxSink[] sinks = new FluxSink[1]; + + rule.setAcceptingSocket( + new AbstractRSocket() { + @Override + public Flux requestStream(Payload payload) { + payload.release(); + return Flux.create(sink -> sinks[0] = sink, FluxSink.OverflowStrategy.IGNORE); + } + }, + Integer.MAX_VALUE); + + rule.sendRequest(1, REQUEST_STREAM); + + ByteBuf cancelFrame = CancelFrameFlyweight.encode(allocator, 1); + FluxSink sink = sinks[0]; + RaceTestUtils.race( + () -> rule.connection.addToReceivedBuffer(cancelFrame), + () -> { + sink.next(ByteBufPayload.create("d1", "m1")); + sink.next(ByteBufPayload.create("d2", "m2")); + sink.next(ByteBufPayload.create("d3", "m3")); + }, + parallel); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + } + + @Test + public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestResponseTest1() { + Scheduler parallel = Schedulers.parallel(); + Hooks.onErrorDropped((e) -> {}); + ByteBufAllocator allocator = rule.alloc(); + for (int i = 0; i < 10000; i++) { + Operators.MonoSubscriber[] sources = new Operators.MonoSubscriber[1]; + + rule.setAcceptingSocket( + new AbstractRSocket() { + @Override + public Mono requestResponse(Payload payload) { + payload.release(); + return new Mono() { + @Override + public void subscribe(CoreSubscriber actual) { + sources[0] = new Operators.MonoSubscriber<>(actual); + actual.onSubscribe(sources[0]); + } + }; + } + }, + Integer.MAX_VALUE); + + rule.sendRequest(1, REQUEST_RESPONSE); + + ByteBuf cancelFrame = CancelFrameFlyweight.encode(allocator, 1); + RaceTestUtils.race( + () -> rule.connection.addToReceivedBuffer(cancelFrame), + () -> { + sources[0].complete(ByteBufPayload.create("d1", "m1")); + }, + parallel); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + } + + @Test + public void simpleDiscardRequestStreamTest() { + ByteBufAllocator allocator = rule.alloc(); + FluxSink[] sinks = new FluxSink[1]; + + rule.setAcceptingSocket( + new AbstractRSocket() { + @Override + public Flux requestStream(Payload payload) { + payload.release(); + return Flux.create(sink -> sinks[0] = sink, FluxSink.OverflowStrategy.IGNORE); + } + }, + 1); + + rule.sendRequest(1, REQUEST_STREAM); + + ByteBuf cancelFrame = CancelFrameFlyweight.encode(allocator, 1); + FluxSink sink = sinks[0]; + + sink.next(ByteBufPayload.create("d1", "m1")); + sink.next(ByteBufPayload.create("d2", "m2")); + sink.next(ByteBufPayload.create("d3", "m3")); + rule.connection.addToReceivedBuffer(cancelFrame); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + + @Test + public void simpleDiscardRequestChannelTest() { + ByteBufAllocator allocator = rule.alloc(); + + rule.setAcceptingSocket( + new AbstractRSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + return (Flux) payloads; + } + }, + 1); + + rule.sendRequest(1, REQUEST_STREAM); + + ByteBuf cancelFrame = CancelFrameFlyweight.encode(allocator, 1); + + ByteBuf metadata1 = allocator.buffer(); + metadata1.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data1 = allocator.buffer(); + data1.writeCharSequence("def", CharsetUtil.UTF_8); + ByteBuf nextFrame1 = + PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata1, data1); + + ByteBuf metadata2 = allocator.buffer(); + metadata2.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data2 = allocator.buffer(); + data2.writeCharSequence("def", CharsetUtil.UTF_8); + ByteBuf nextFrame2 = + PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata2, data2); + + ByteBuf metadata3 = allocator.buffer(); + metadata3.writeCharSequence("abc", CharsetUtil.UTF_8); + ByteBuf data3 = allocator.buffer(); + data3.writeCharSequence("def", CharsetUtil.UTF_8); + ByteBuf nextFrame3 = + PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata3, data3); + rule.connection.addToReceivedBuffer(nextFrame1, nextFrame2, nextFrame3); + + rule.connection.addToReceivedBuffer(cancelFrame); + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); } public static class ServerSocketRule extends AbstractSocketRule { private RSocket acceptingSocket; + private volatile int prefetch; @Override protected void init() { @@ -188,25 +625,26 @@ public void setAcceptingSocket(RSocket acceptingSocket) { connection = new TestDuplexConnection(); connectSub = TestSubscriber.create(); errors = new ConcurrentLinkedQueue<>(); + this.prefetch = Integer.MAX_VALUE; super.init(); } public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { this.acceptingSocket = acceptingSocket; connection = new TestDuplexConnection(); - connection.setInitialSendRequestN(prefetch); connectSub = TestSubscriber.create(); errors = new ConcurrentLinkedQueue<>(); + this.prefetch = prefetch; super.init(); } @Override - protected RSocketResponder newRSocket() { + protected RSocketResponder newRSocket(LeaksTrackingByteBufAllocator allocator) { return new RSocketResponder( - ByteBufAllocator.DEFAULT, + allocator, connection, acceptingSocket, - DefaultPayload::create, + PayloadDecoder.ZERO_COPY, throwable -> errors.add(throwable), ResponderLeaseHandler.None, 0); @@ -219,25 +657,34 @@ private void sendRequest(int streamId, FrameType frameType) { case REQUEST_CHANNEL: request = RequestChannelFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, streamId, false, false, 1, EmptyPayload.INSTANCE); + allocator, + streamId, + false, + false, + prefetch, + Unpooled.EMPTY_BUFFER, + Unpooled.EMPTY_BUFFER); break; case REQUEST_STREAM: request = RequestStreamFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, streamId, false, 1, EmptyPayload.INSTANCE); + allocator, + streamId, + false, + prefetch, + Unpooled.EMPTY_BUFFER, + Unpooled.EMPTY_BUFFER); break; case REQUEST_RESPONSE: request = RequestResponseFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, streamId, false, EmptyPayload.INSTANCE); + allocator, streamId, false, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); break; default: throw new IllegalArgumentException("unsupported type: " + frameType); } connection.addToReceivedBuffer(request); - connection.addToReceivedBuffer( - RequestNFrameFlyweight.encode(ByteBufAllocator.DEFAULT, streamId, 2)); } } } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java b/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java index b22a95c0b..5e94935c5 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java @@ -17,6 +17,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; +import io.netty.util.IllegalReferenceCountException; import org.assertj.core.presentation.StandardRepresentation; public final class ByteBufRepresentation extends StandardRepresentation { @@ -24,7 +25,11 @@ public final class ByteBufRepresentation extends StandardRepresentation { @Override protected String fallbackToStringOf(Object object) { if (object instanceof ByteBuf) { - return ByteBufUtil.prettyHexDump((ByteBuf) object); + try { + return ByteBufUtil.prettyHexDump((ByteBuf) object); + } catch (IllegalReferenceCountException e) { + // noops + } } return super.fallbackToStringOf(object); From 49e93c95efc626b4abdd62e3bf96f3ce9bb40546 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 21 Apr 2020 23:13:56 +0300 Subject: [PATCH 140/181] improves test logging Signed-off-by: Oleh Dokuka --- build.gradle | 52 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/build.gradle b/build.gradle index 7e5eb9823..dd5135f01 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -26,7 +26,7 @@ plugins { subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - + ext['reactor-bom.version'] = 'Dysprosium-SR6' ext['logback.version'] = '1.2.3' ext['findbugs.version'] = '3.0.2' @@ -109,25 +109,61 @@ subprojects { } javadoc { + def jdk = JavaVersion.current().majorVersion + def jdkJavadoc = "https://docs.oracle.com/javase/$jdk/docs/api/" + if (JavaVersion.current().isJava11Compatible()) { + jdkJavadoc = "https://docs.oracle.com/en/java/javase/$jdk/docs/api/" + } options.with { - links 'https://docs.oracle.com/javase/8/docs/api/' + links jdkJavadoc links 'https://projectreactor.io/docs/core/release/api/' links 'https://netty.io/4.1/api/' } } + tasks.named("javadoc").configure { + onlyIf { System.getenv('SKIP_RELEASE') != "true" } + } + test { useJUnitPlatform() + systemProperty "io.netty.leakDetection.level", "ADVANCED" + } + + //all test tasks will show FAILED for each test method, + // common exclusions, no scanning + project.tasks.withType(Test).all { testLogging { - events "started", "passed", "skipped", "failed" + events "FAILED" + showExceptions true + exceptionFormat "FULL" + stackTraceFilters "ENTRY_POINT" + maxGranularity 3 } - systemProperty "io.netty.leakDetection.level", "ADVANCED" - } + if (JavaVersion.current().isJava9Compatible()) { + println "Java 9+: lowering MaxGCPauseMillis to 20ms in ${project.name} ${name}" + jvmArgs = ["-XX:MaxGCPauseMillis=20"] + } - tasks.named("javadoc").configure { - onlyIf { System.getenv('SKIP_RELEASE') != "true" } + systemProperty("java.awt.headless", "true") + systemProperty("reactor.trace.cancel", "true") + systemProperty("reactor.trace.nocapacity", "true") + systemProperty("testGroups", project.properties.get("testGroups")) + scanForTestClasses = false + exclude '**/*Abstract*.*' + + //allow re-run of failed tests only without special test tasks failing + // because the filter is too restrictive + filter.setFailOnNoMatchingTests(false) + + //display intermediate results for special test tasks + afterSuite { desc, result -> + if (!desc.parent) { // will match the outermost suite + println('\n' + "${desc} Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)") + } + } } } From 91e894a2ca57ebe4f81f3bca4c526cf9078b0f86 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 21 Apr 2020 23:56:45 +0300 Subject: [PATCH 141/181] removes redundant frames being sent (#792) Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/RSocketRequester.java | 4 +- .../io/rsocket/core/RSocketResponder.java | 40 ++++++++++++++----- .../io/rsocket/core/RSocketRequesterTest.java | 6 +-- .../io/rsocket/core/RSocketResponderTest.java | 36 +++++++++-------- 4 files changed, 53 insertions(+), 33 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 70b2a1889..5bb2890c1 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -261,9 +261,7 @@ public void doOnTerminal( @Nonnull SignalType signalType, @Nullable Payload element, @Nullable Throwable e) { - if (signalType == SignalType.ON_ERROR) { - sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, e)); - } else if (signalType == SignalType.CANCEL) { + if (signalType == SignalType.CANCEL) { sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); } removeStreamReceiver(streamId); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index e01000e49..563cfe624 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -36,6 +36,7 @@ import io.rsocket.lease.ResponderLeaseHandler; import java.util.function.Consumer; import java.util.function.LongConsumer; +import javax.annotation.Nullable; import org.reactivestreams.Processor; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -302,7 +303,7 @@ private void handleFrame(ByteBuf frame) { case REQUEST_STREAM: int streamInitialRequestN = RequestStreamFrameFlyweight.initialRequestN(frame); Payload streamPayload = payloadDecoder.apply(frame); - handleStream(streamId, requestStream(streamPayload), streamInitialRequestN); + handleStream(streamId, requestStream(streamPayload), streamInitialRequestN, null); break; case REQUEST_CHANNEL: int channelInitialRequestN = RequestChannelFrameFlyweight.initialRequestN(frame); @@ -433,7 +434,11 @@ protected void hookFinally(SignalType type) { response.doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER).subscribe(subscriber); } - private void handleStream(int streamId, Flux response, int initialRequestN) { + private void handleStream( + int streamId, + Flux response, + int initialRequestN, + @Nullable UnicastProcessor requestChannel) { final BaseSubscriber subscriber = new BaseSubscriber() { @@ -446,6 +451,17 @@ protected void hookOnSubscribe(Subscription s) { protected void hookOnNext(Payload payload) { if (!PayloadValidationUtils.isValid(mtu, payload)) { payload.release(); + // specifically for requestChannel case so when Payload is invalid we will not be + // sending CancelFrame and ErrorFrame + // Note: CancelFrame is redundant and due to spec + // (https://github.com/rsocket/rsocket/blob/master/Protocol.md#request-channel) + // Upon receiving an ERROR[APPLICATION_ERROR|REJECTED|CANCELED|INVALID], the stream is + // terminated on both Requester and Responder. + // Upon sending an ERROR[APPLICATION_ERROR|REJECTED|CANCELED|INVALID], the stream is + // terminated on both the Requester and Responder. + if (requestChannel != null) { + channelProcessors.remove(streamId, requestChannel); + } cancel(); final IllegalArgumentException t = new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); @@ -495,9 +511,6 @@ private void handleChannel(int streamId, Payload payload, int initialRequestN) { Flux payloads = frames - .doOnCancel( - () -> sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId))) - .doOnError(t -> handleError(streamId, t)) .doOnRequest( new LongConsumer() { boolean first = true; @@ -511,10 +524,19 @@ public void accept(long l) { } else { n = l; } - sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); + if (n > 0) { + sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); + } + } + }) + .doFinally( + signalType -> { + if (channelProcessors.remove(streamId, frames)) { + if (signalType == SignalType.CANCEL) { + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + } } }) - .doFinally(signalType -> channelProcessors.remove(streamId)) .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); // not chained, as the payload should be enqueued in the Unicast processor before this method @@ -523,9 +545,9 @@ public void accept(long l) { frames.onNext(payload); if (responderRSocket != null) { - handleStream(streamId, requestChannel(payload, payloads), initialRequestN); + handleStream(streamId, requestChannel(payload, payloads), initialRequestN, frames); } else { - handleStream(streamId, requestChannel(payloads), initialRequestN); + handleStream(streamId, requestChannel(payloads), initialRequestN, frames); } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index 586c9cfd3..36abb14b4 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -174,8 +174,8 @@ public void testHandleApplicationException() { verify(responseSub).onError(any(ApplicationErrorException.class)); Assertions.assertThat(rule.connection.getSent()) - // requestResponseFrame FIXME - // .hasSize(1) + // requestResponseFrame + .hasSize(1) .allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); @@ -356,8 +356,6 @@ public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmen .isInstanceOf(IllegalArgumentException.class) .hasMessage(INVALID_PAYLOAD_ERROR_MESSAGE)) .verify(); - // FIXME: should be removed - Assertions.assertThat(rule.connection.getSent()).allMatch(bb -> bb.release()); rule.assertHasNoLeaks(); }); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index d31fc3bf7..05f9fd46e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -68,7 +68,9 @@ import org.junit.runners.model.Statement; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; import reactor.core.CoreSubscriber; +import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; import reactor.core.publisher.Hooks; @@ -193,27 +195,27 @@ public Flux requestStream(Payload p) { p.release(); return Flux.just(payload).doOnCancel(() -> cancelled.set(true)); } - // FIXME - // @Override - // public Flux requestChannel(Publisher payloads) { - // Flux.from(payloads) - // .doOnNext(Payload::release) - // .subscribe( - // new BaseSubscriber() { - // @Override - // protected void hookOnSubscribe(Subscription subscription) { - // subscription.request(1); - // } - // }); - // return Flux.just(payload).doOnCancel(() -> cancelled.set(true)); - // } + + @Override + public Flux requestChannel(Publisher payloads) { + Flux.from(payloads) + .doOnNext(Payload::release) + .subscribe( + new BaseSubscriber() { + @Override + protected void hookOnSubscribe(Subscription subscription) { + subscription.request(1); + } + }); + return Flux.just(payload).doOnCancel(() -> cancelled.set(true)); + } }; rule.setAcceptingSocket(acceptingSocket); final Runnable[] runnables = { () -> rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE), - () -> rule.sendRequest(streamId, FrameType.REQUEST_STREAM) /* FIXME, - () -> rule.sendRequest(streamId, FrameType.REQUEST_CHANNEL)*/ + () -> rule.sendRequest(streamId, FrameType.REQUEST_STREAM), + () -> rule.sendRequest(streamId, FrameType.REQUEST_CHANNEL) }; for (Runnable runnable : runnables) { @@ -224,9 +226,9 @@ public Flux requestStream(Payload p) { .isInstanceOf(IllegalArgumentException.class) .hasToString("java.lang.IllegalArgumentException: " + INVALID_PAYLOAD_ERROR_MESSAGE); Assertions.assertThat(rule.connection.getSent()) - .filteredOn(bb -> FrameHeaderFlyweight.frameType(bb) == FrameType.ERROR) .hasSize(1) .first() + .matches(bb -> FrameHeaderFlyweight.frameType(bb) == FrameType.ERROR) .matches(bb -> ErrorFrameFlyweight.dataUtf8(bb).contains(INVALID_PAYLOAD_ERROR_MESSAGE)) .matches(ReferenceCounted::release); From 5ee6c38d472939a3d2189605606988146954168f Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Wed, 22 Apr 2020 20:52:35 +0300 Subject: [PATCH 142/181] fixes incorrect request propagation Initially that issue was hidden because both sides uses limitRate which does prefetch 256 elements in advance so it is almost impossible to track underflow in request Signed-off-by: Oleh Dokuka --- .../io/rsocket/core/RSocketRequester.java | 3 +++ .../java/io/rsocket/core/RSocketTest.java | 27 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 5bb2890c1..d95015fae 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -382,7 +382,10 @@ protected void hookOnSubscribe(Subscription subscription) { protected void hookOnNext(Payload payload) { if (first) { // need to skip first since we have already sent it + // no need to release it since it was released earlier on the request establishment + // phase first = false; + request(1); return; } if (!PayloadValidationUtils.isValid(mtu, payload)) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index 376614070..f0ae4ffd5 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -34,6 +34,7 @@ import io.rsocket.test.util.TestSubscriber; import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; +import java.time.Duration; import java.util.ArrayList; import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.Assertions; @@ -120,6 +121,32 @@ public Mono requestResponse(Payload payload) { rule.assertServerError("CustomRSocketException (0x501): Deliberate Custom exception."); } + @Test(timeout = 2000) + public void testRequestPropagatesCorrectlyForRequestChannel() { + rule.setRequestAcceptor( + new AbstractRSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + // specifically limits request to 3 in order to prevent 256 request from limitRate + // hidden on the responder side + .limitRequest(3); + } + }); + + Flux.range(0, 3) + .map(i -> DefaultPayload.create("" + i)) + .as(rule.crs::requestChannel) + .as(publisher -> StepVerifier.create(publisher, 3)) + .expectSubscription() + .expectNextCount(3) + .expectComplete() + .verify(Duration.ofMillis(5000)); + + rule.assertNoClientErrors(); + rule.assertNoServerErrors(); + } + @Test(timeout = 2000) public void testStream() throws Exception { Flux responses = rule.crs.requestStream(DefaultPayload.create("Payload In")); From 480b5018c0660013a251faf13626c65ae408b640 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 23 Apr 2020 13:03:50 +0300 Subject: [PATCH 143/181] fixes requestChannel and ensure support of half-closed state (#794) The following is the cases which MUST be supported OUTER PUBLISHER COMPLETED - INNER PUBLISHER CAN SEND OUTER PUBLISHER CANCELLED - INNER PUBLISHER CAN SEND INNER PUBLISHER COMPLETED - OUTER PUBLISHER CAN SEND INNER PUBLISHER CANCELLED - the WHOLE CHAIN IS TERMINATED --- .../io/rsocket/core/RSocketRequester.java | 84 ++++--- .../io/rsocket/core/RSocketResponder.java | 19 ++ .../java/io/rsocket/core/RSocketTest.java | 237 +++++++++++++++++- 3 files changed, 302 insertions(+), 38 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index d95015fae..4d7d47f6a 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -560,46 +560,58 @@ private void handleStreamZero(FrameType type, ByteBuf frame) { private void handleFrame(int streamId, FrameType type, ByteBuf frame) { Subscriber receiver = receivers.get(streamId); - if (receiver == null) { - handleMissingResponseProcessor(streamId, type, frame); - } else { - switch (type) { - case ERROR: - receiver.onError(Exceptions.from(streamId, frame)); - receivers.remove(streamId); - break; - case NEXT_COMPLETE: - receiver.onNext(payloadDecoder.apply(frame)); - receiver.onComplete(); - break; - case CANCEL: - { - Subscription sender = senders.remove(streamId); - if (sender != null) { - sender.cancel(); - } - break; + switch (type) { + case NEXT: + if (receiver == null) { + handleMissingResponseProcessor(streamId, type, frame); + return; + } + receiver.onNext(payloadDecoder.apply(frame)); + break; + case NEXT_COMPLETE: + if (receiver == null) { + handleMissingResponseProcessor(streamId, type, frame); + return; + } + receiver.onNext(payloadDecoder.apply(frame)); + receiver.onComplete(); + break; + case COMPLETE: + if (receiver == null) { + handleMissingResponseProcessor(streamId, type, frame); + return; + } + receiver.onComplete(); + receivers.remove(streamId); + break; + case ERROR: + if (receiver == null) { + handleMissingResponseProcessor(streamId, type, frame); + return; + } + receiver.onError(Exceptions.from(streamId, frame)); + receivers.remove(streamId); + break; + case CANCEL: + { + Subscription sender = senders.remove(streamId); + if (sender != null) { + sender.cancel(); } - case NEXT: - receiver.onNext(payloadDecoder.apply(frame)); break; - case REQUEST_N: - { - Subscription sender = senders.get(streamId); - if (sender != null) { - int n = RequestNFrameFlyweight.requestN(frame); - sender.request(n >= Integer.MAX_VALUE ? Long.MAX_VALUE : n); - } - break; + } + case REQUEST_N: + { + Subscription sender = senders.get(streamId); + if (sender != null) { + int n = RequestNFrameFlyweight.requestN(frame); + sender.request(n >= Integer.MAX_VALUE ? Long.MAX_VALUE : n); } - case COMPLETE: - receiver.onComplete(); - receivers.remove(streamId); break; - default: - throw new IllegalStateException( - "Client received supported frame on stream " + streamId + ": " + frame.toString()); - } + } + default: + throw new IllegalStateException( + "Client received supported frame on stream " + streamId + ": " + frame.toString()); } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index 563cfe624..f9c3d1c65 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -492,6 +492,24 @@ protected void hookOnError(Throwable throwable) { handleError(streamId, throwable); } + @Override + protected void hookOnCancel() { + // specifically for requestChannel case so when requester sends Cancel frame so the + // whole chain MUST be terminated + // Note: CancelFrame is redundant from the responder side due to spec + // (https://github.com/rsocket/rsocket/blob/master/Protocol.md#request-channel) + // Upon receiving a CANCEL, the stream is terminated on the Responder. + // Upon sending a CANCEL, the stream is terminated on the Requester. + if (requestChannel != null) { + channelProcessors.remove(streamId, requestChannel); + try { + requestChannel.dispose(); + } catch (Exception e) { + // might be thrown back if stream is cancelled + } + } + } + @Override protected void hookFinally(SignalType type) { sendingSubscriptions.remove(streamId); @@ -568,6 +586,7 @@ protected void hookOnError(Throwable throwable) { private void handleCancelFrame(int streamId) { Subscription subscription = sendingSubscriptions.remove(streamId); + channelProcessors.remove(streamId); if (subscription != null) { subscription.cancel(); diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index f0ae4ffd5..f29f4409c 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -28,6 +28,8 @@ import io.rsocket.RSocket; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.CustomRSocketException; +import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.subscriber.AssertSubscriber; import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.lease.ResponderLeaseHandler; import io.rsocket.test.util.LocalDuplexConnection; @@ -36,6 +38,7 @@ import io.rsocket.util.EmptyPayload; import java.time.Duration; import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.Assertions; import org.hamcrest.MatcherAssert; @@ -52,6 +55,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import reactor.test.publisher.TestPublisher; public class RSocketTest { @@ -177,6 +181,235 @@ public Flux requestChannel(Publisher payloads) { Assertions.assertThat(error.get()).isNull(); } + @Test + public void requestChannelCase_StreamIsTerminatedAfterBothSidesSentCompletion1() { + TestPublisher outerPublisher = TestPublisher.create(); + AssertSubscriber outerAssertSubscriber = new AssertSubscriber<>(0); + + AssertSubscriber innerAssertSubscriber = new AssertSubscriber<>(0); + TestPublisher innerPublisher = TestPublisher.create(); + + initRequestChannelCase( + outerPublisher, outerAssertSubscriber, innerPublisher, innerAssertSubscriber); + + nextFromOuterPublisher(outerPublisher, innerAssertSubscriber); + + completeFromOuterPublisher(outerPublisher, innerAssertSubscriber); + + nextFromInnerPublisher(innerPublisher, outerAssertSubscriber); + + completeFromInnerPublisher(innerPublisher, outerAssertSubscriber); + } + + @Test + public void requestChannelCase_StreamIsTerminatedAfterBothSidesSentCompletion2() { + TestPublisher outerPublisher = TestPublisher.create(); + AssertSubscriber outerAssertSubscriber = new AssertSubscriber<>(0); + + AssertSubscriber innerAssertSubscriber = new AssertSubscriber<>(0); + TestPublisher innerPublisher = TestPublisher.create(); + + initRequestChannelCase( + outerPublisher, outerAssertSubscriber, innerPublisher, innerAssertSubscriber); + + nextFromInnerPublisher(innerPublisher, outerAssertSubscriber); + + completeFromInnerPublisher(innerPublisher, outerAssertSubscriber); + + nextFromOuterPublisher(outerPublisher, innerAssertSubscriber); + + completeFromOuterPublisher(outerPublisher, innerAssertSubscriber); + } + + @Test + public void + requestChannelCase_CancellationFromResponderShouldLeaveStreamInHalfClosedStateWithNextCompletionPossibleFromRequester() { + TestPublisher outerPublisher = TestPublisher.create(); + AssertSubscriber outerAssertSubscriber = new AssertSubscriber<>(0); + + AssertSubscriber innerAssertSubscriber = new AssertSubscriber<>(0); + TestPublisher innerPublisher = TestPublisher.create(); + + initRequestChannelCase( + outerPublisher, outerAssertSubscriber, innerPublisher, innerAssertSubscriber); + + nextFromOuterPublisher(outerPublisher, innerAssertSubscriber); + + cancelFromInnerSubscriber(outerPublisher, innerAssertSubscriber); + + nextFromInnerPublisher(innerPublisher, outerAssertSubscriber); + + completeFromInnerPublisher(innerPublisher, outerAssertSubscriber); + } + + @Test + public void + requestChannelCase_CompletionFromRequesterShouldLeaveStreamInHalfClosedStateWithNextCancellationPossibleFromResponder() { + TestPublisher outerPublisher = TestPublisher.create(); + AssertSubscriber outerAssertSubscriber = new AssertSubscriber<>(0); + + AssertSubscriber innerAssertSubscriber = new AssertSubscriber<>(0); + TestPublisher innerPublisher = TestPublisher.create(); + + initRequestChannelCase( + outerPublisher, outerAssertSubscriber, innerPublisher, innerAssertSubscriber); + + nextFromInnerPublisher(innerPublisher, outerAssertSubscriber); + + completeFromInnerPublisher(innerPublisher, outerAssertSubscriber); + + nextFromOuterPublisher(outerPublisher, innerAssertSubscriber); + + cancelFromInnerSubscriber(outerPublisher, innerAssertSubscriber); + } + + @Test + public void + requestChannelCase_ensureThatRequesterSubscriberCancellationTerminatesStreamsOnBothSides() { + TestPublisher outerPublisher = TestPublisher.create(); + AssertSubscriber outerAssertSubscriber = new AssertSubscriber<>(0); + + AssertSubscriber innerAssertSubscriber = new AssertSubscriber<>(0); + TestPublisher innerPublisher = TestPublisher.create(); + + initRequestChannelCase( + outerPublisher, outerAssertSubscriber, innerPublisher, innerAssertSubscriber); + + nextFromInnerPublisher(innerPublisher, outerAssertSubscriber); + + nextFromOuterPublisher(outerPublisher, innerAssertSubscriber); + + // ensures both sides are terminated + cancelFromOuterSubscriber( + outerPublisher, outerAssertSubscriber, innerPublisher, innerAssertSubscriber); + } + + void initRequestChannelCase( + TestPublisher outerPublisher, + AssertSubscriber outerAssertSubscriber, + TestPublisher innerPublisher, + AssertSubscriber innerAssertSubscriber) { + rule.setRequestAcceptor( + new AbstractRSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + payloads.subscribe(innerAssertSubscriber); + return innerPublisher.flux(); + } + }); + + rule.crs.requestChannel(outerPublisher).subscribe(outerAssertSubscriber); + + outerPublisher.assertWasSubscribed(); + outerAssertSubscriber.assertSubscribed(); + + innerAssertSubscriber.assertNotSubscribed(); + innerPublisher.assertWasNotSubscribed(); + + // firstRequest + outerAssertSubscriber.request(1); + outerPublisher.assertMaxRequested(1); + outerPublisher.next(DefaultPayload.create("initialData", "initialMetadata")); + + innerAssertSubscriber.assertSubscribed(); + innerPublisher.assertWasSubscribed(); + } + + void nextFromOuterPublisher( + TestPublisher outerPublisher, AssertSubscriber innerAssertSubscriber) { + // ensures that outerUpstream and innerSubscriber is not terminated so the requestChannel + outerPublisher.assertSubscribers(1); + innerAssertSubscriber.assertNotTerminated(); + + innerAssertSubscriber.request(6); + outerPublisher.next( + DefaultPayload.create("d1", "m1"), + DefaultPayload.create("d2"), + DefaultPayload.create("d3", "m3"), + DefaultPayload.create("d4"), + DefaultPayload.create("d5", "m5")); + + List innerPayloads = innerAssertSubscriber.awaitAndAssertNextValueCount(6).values(); + Assertions.assertThat(innerPayloads.stream().map(Payload::getDataUtf8)) + .containsExactly("initialData", "d1", "d2", "d3", "d4", "d5"); + // fixme: incorrect behaviour of metadata encoding + // Assertions + // .assertThat(innerPayloads + // .stream() + // .map(Payload::hasMetadata) + // ) + // .containsExactly(true, true, false, true, false, true); + Assertions.assertThat(innerPayloads.stream().map(Payload::getMetadataUtf8)) + .containsExactly("initialMetadata", "m1", "", "m3", "", "m5"); + } + + void completeFromOuterPublisher( + TestPublisher outerPublisher, AssertSubscriber innerAssertSubscriber) { + // ensures that after sending complete upstream part is closed + outerPublisher.complete(); + innerAssertSubscriber.assertTerminated(); + outerPublisher.assertNoSubscribers(); + } + + void cancelFromInnerSubscriber( + TestPublisher outerPublisher, AssertSubscriber innerAssertSubscriber) { + // ensures that after sending complete upstream part is closed + innerAssertSubscriber.cancel(); + outerPublisher.assertWasCancelled(); + outerPublisher.assertNoSubscribers(); + } + + void nextFromInnerPublisher( + TestPublisher innerPublisher, AssertSubscriber outerAssertSubscriber) { + // ensures that downstream is not terminated so the requestChannel state is half-closed + innerPublisher.assertSubscribers(1); + outerAssertSubscriber.assertNotTerminated(); + + // ensures innerPublisher can send messages and outerSubscriber can receive them + outerAssertSubscriber.request(5); + innerPublisher.next( + DefaultPayload.create("rd1", "rm1"), + DefaultPayload.create("rd2"), + DefaultPayload.create("rd3", "rm3"), + DefaultPayload.create("rd4"), + DefaultPayload.create("rd5", "rm5")); + + List outerPayloads = outerAssertSubscriber.awaitAndAssertNextValueCount(5).values(); + Assertions.assertThat(outerPayloads.stream().map(Payload::getDataUtf8)) + .containsExactly("rd1", "rd2", "rd3", "rd4", "rd5"); + // fixme: incorrect behaviour of metadata encoding + // Assertions + // .assertThat(outerPayloads + // .stream() + // .map(Payload::hasMetadata) + // ) + // .containsExactly(true, false, true, false, true); + Assertions.assertThat(outerPayloads.stream().map(Payload::getMetadataUtf8)) + .containsExactly("rm1", "", "rm3", "", "rm5"); + } + + void completeFromInnerPublisher( + TestPublisher innerPublisher, AssertSubscriber outerAssertSubscriber) { + // ensures that after sending complete inner upstream is closed + innerPublisher.complete(); + outerAssertSubscriber.assertTerminated(); + innerPublisher.assertNoSubscribers(); + } + + void cancelFromOuterSubscriber( + TestPublisher outerPublisher, + AssertSubscriber outerAssertSubscriber, + TestPublisher innerPublisher, + AssertSubscriber innerAssertSubscriber) { + // ensures that after sending cancel the whole requestChannel is terminated + outerAssertSubscriber.cancel(); + innerPublisher.assertWasCancelled(); + innerPublisher.assertNoSubscribers(); + // ensures that cancellation is propagated to the actual upstream + outerPublisher.assertWasCancelled(); + outerPublisher.assertNoSubscribers(); + } + public static class SocketRule extends ExternalResource { DirectProcessor serverProcessor; @@ -246,7 +479,7 @@ public Flux requestChannel(Publisher payloads) { ByteBufAllocator.DEFAULT, serverConnection, requestAcceptor, - DefaultPayload::create, + PayloadDecoder.DEFAULT, throwable -> serverErrors.add(throwable), ResponderLeaseHandler.None, 0); @@ -255,7 +488,7 @@ public Flux requestChannel(Publisher payloads) { new RSocketRequester( ByteBufAllocator.DEFAULT, clientConnection, - DefaultPayload::create, + PayloadDecoder.DEFAULT, throwable -> clientErrors.add(throwable), StreamIdSupplier.clientSupplier(), 0, From cd67e541d5eb64b72a9df330ec15ff015d0c4e49 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 23 Apr 2020 19:40:44 +0100 Subject: [PATCH 144/181] Drop UriHandler and UriTransportRegistry (#795) Closes gh-782 Signed-off-by: Rossen Stoyanchev --- .../main/java/io/rsocket/uri/UriHandler.java | 58 ------------- .../io/rsocket/uri/UriTransportRegistry.java | 87 ------------------- .../java/io/rsocket/uri/TestUriHandler.java | 46 ---------- .../rsocket/uri/UriTransportRegistryTest.java | 42 --------- .../services/io.rsocket.uri.UriHandler | 17 ---- .../java/io/rsocket/test/UriHandlerTest.java | 74 ---------------- .../transport/local/LocalUriHandler.java | 55 ------------ .../services/io.rsocket.uri.UriHandler | 17 ---- .../transport/local/LocalUriHandlerTest.java | 38 -------- .../local/LocalUriTransportRegistryTest.java | 54 ------------ .../transport/netty/TcpUriHandler.java | 59 ------------- .../transport/netty/WebsocketUriHandler.java | 64 -------------- .../services/io.rsocket.uri.UriHandler | 18 ---- .../transport/netty/TcpUriHandlerTest.java | 38 -------- .../netty/TcpUriTransportRegistryTest.java | 60 ------------- .../netty/WebsocketUriHandlerTest.java | 38 -------- .../WebsocketUriTransportRegistryTest.java | 60 ------------- 17 files changed, 825 deletions(-) delete mode 100644 rsocket-core/src/main/java/io/rsocket/uri/UriHandler.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/uri/UriTransportRegistry.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/uri/TestUriHandler.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/uri/UriTransportRegistryTest.java delete mode 100644 rsocket-core/src/test/resources/META-INF/services/io.rsocket.uri.UriHandler delete mode 100644 rsocket-test/src/main/java/io/rsocket/test/UriHandlerTest.java delete mode 100644 rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalUriHandler.java delete mode 100644 rsocket-transport-local/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler delete mode 100644 rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriHandlerTest.java delete mode 100644 rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriTransportRegistryTest.java delete mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpUriHandler.java delete mode 100644 rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketUriHandler.java delete mode 100644 rsocket-transport-netty/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler delete mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpUriHandlerTest.java delete mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpUriTransportRegistryTest.java delete mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriHandlerTest.java delete mode 100644 rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriTransportRegistryTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/uri/UriHandler.java b/rsocket-core/src/main/java/io/rsocket/uri/UriHandler.java deleted file mode 100644 index ec3d4ab3c..000000000 --- a/rsocket-core/src/main/java/io/rsocket/uri/UriHandler.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2015-2018 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.uri; - -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import java.net.URI; -import java.util.Optional; -import java.util.ServiceLoader; - -/** Maps a {@link URI} to a {@link ClientTransport} or {@link ServerTransport}. */ -public interface UriHandler { - - /** - * Load all registered instances of {@code UriHandler}. - * - * @return all registered instances of {@code UriHandler} - */ - static ServiceLoader loadServices() { - return ServiceLoader.load(UriHandler.class); - } - - /** - * Returns an implementation of {@link ClientTransport} unambiguously mapped to a {@link URI}, - * otherwise {@link Optional#EMPTY}. - * - * @param uri the uri to map - * @return an implementation of {@link ClientTransport} unambiguously mapped to a {@link URI}, * - * otherwise {@link Optional#EMPTY} - * @throws NullPointerException if {@code uri} is {@code null} - */ - Optional buildClient(URI uri); - - /** - * Returns an implementation of {@link ServerTransport} unambiguously mapped to a {@link URI}, - * otherwise {@link Optional#EMPTY}. - * - * @param uri the uri to map - * @return an implementation of {@link ServerTransport} unambiguously mapped to a {@link URI}, * - * otherwise {@link Optional#EMPTY} - * @throws NullPointerException if {@code uri} is {@code null} - */ - Optional buildServer(URI uri); -} diff --git a/rsocket-core/src/main/java/io/rsocket/uri/UriTransportRegistry.java b/rsocket-core/src/main/java/io/rsocket/uri/UriTransportRegistry.java deleted file mode 100644 index 204c5d1ea..000000000 --- a/rsocket-core/src/main/java/io/rsocket/uri/UriTransportRegistry.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2015-2018 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.uri; - -import static io.rsocket.uri.UriHandler.loadServices; - -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import java.net.URI; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import java.util.ServiceLoader; -import reactor.core.publisher.Mono; - -/** - * Registry for looking up transports by URI. - * - *

    Uses the Jar Services mechanism with services defined by {@link UriHandler}. - */ -public class UriTransportRegistry { - private static final ClientTransport FAILED_CLIENT_LOOKUP = - (mtu) -> Mono.error(new UnsupportedOperationException()); - private static final ServerTransport FAILED_SERVER_LOOKUP = - (acceptor, mtu) -> Mono.error(new UnsupportedOperationException()); - - private List handlers; - - public UriTransportRegistry(ServiceLoader services) { - handlers = new ArrayList<>(); - services.forEach(handlers::add); - } - - public static UriTransportRegistry fromServices() { - ServiceLoader services = loadServices(); - - return new UriTransportRegistry(services); - } - - public static ClientTransport clientForUri(String uri) { - return UriTransportRegistry.fromServices().findClient(uri); - } - - public static ServerTransport serverForUri(String uri) { - return UriTransportRegistry.fromServices().findServer(uri); - } - - private ClientTransport findClient(String uriString) { - URI uri = URI.create(uriString); - - for (UriHandler h : handlers) { - Optional r = h.buildClient(uri); - if (r.isPresent()) { - return r.get(); - } - } - - return FAILED_CLIENT_LOOKUP; - } - - private ServerTransport findServer(String uriString) { - URI uri = URI.create(uriString); - - for (UriHandler h : handlers) { - Optional r = h.buildServer(uri); - if (r.isPresent()) { - return r.get(); - } - } - - return FAILED_SERVER_LOOKUP; - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/uri/TestUriHandler.java b/rsocket-core/src/test/java/io/rsocket/uri/TestUriHandler.java deleted file mode 100644 index 526757fbe..000000000 --- a/rsocket-core/src/test/java/io/rsocket/uri/TestUriHandler.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2015-2018 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.uri; - -import io.rsocket.test.util.TestDuplexConnection; -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import java.net.URI; -import java.util.Objects; -import java.util.Optional; -import reactor.core.publisher.Mono; - -public final class TestUriHandler implements UriHandler { - - private static final String SCHEME = "test"; - - @Override - public Optional buildClient(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (!SCHEME.equals(uri.getScheme())) { - return Optional.empty(); - } - - return Optional.of((mtu) -> Mono.just(new TestDuplexConnection())); - } - - @Override - public Optional buildServer(URI uri) { - return Optional.empty(); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/uri/UriTransportRegistryTest.java b/rsocket-core/src/test/java/io/rsocket/uri/UriTransportRegistryTest.java deleted file mode 100644 index 7aeef708f..000000000 --- a/rsocket-core/src/test/java/io/rsocket/uri/UriTransportRegistryTest.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2015-2018 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.uri; - -import static org.junit.Assert.assertTrue; - -import io.rsocket.DuplexConnection; -import io.rsocket.test.util.TestDuplexConnection; -import io.rsocket.transport.ClientTransport; -import org.junit.Test; - -public class UriTransportRegistryTest { - @Test - public void testTestRegistered() { - ClientTransport test = UriTransportRegistry.clientForUri("test://test"); - - DuplexConnection duplexConnection = test.connect(0).block(); - - assertTrue(duplexConnection instanceof TestDuplexConnection); - } - - @Test(expected = UnsupportedOperationException.class) - public void testTestUnregistered() { - ClientTransport test = UriTransportRegistry.clientForUri("mailto://bonson@baulsupp.net"); - - test.connect(0).block(); - } -} diff --git a/rsocket-core/src/test/resources/META-INF/services/io.rsocket.uri.UriHandler b/rsocket-core/src/test/resources/META-INF/services/io.rsocket.uri.UriHandler deleted file mode 100644 index 068667aa7..000000000 --- a/rsocket-core/src/test/resources/META-INF/services/io.rsocket.uri.UriHandler +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2015-2018 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. -# - -io.rsocket.uri.TestUriHandler diff --git a/rsocket-test/src/main/java/io/rsocket/test/UriHandlerTest.java b/rsocket-test/src/main/java/io/rsocket/test/UriHandlerTest.java deleted file mode 100644 index ad45e106a..000000000 --- a/rsocket-test/src/main/java/io/rsocket/test/UriHandlerTest.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2015-2018 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.test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatNullPointerException; - -import io.rsocket.uri.UriHandler; -import java.net.URI; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -public interface UriHandlerTest { - - @DisplayName("returns empty Optional client with invalid URI") - @Test - default void buildClientInvalidUri() { - assertThat(getUriHandler().buildClient(URI.create(getInvalidUri()))).isEmpty(); - } - - @DisplayName("buildClient throws NullPointerException with null uri") - @Test - default void buildClientNullUri() { - assertThatNullPointerException() - .isThrownBy(() -> getUriHandler().buildClient(null)) - .withMessage("uri must not be null"); - } - - @DisplayName("returns client with value URI") - @Test - default void buildClientValidUri() { - assertThat(getUriHandler().buildClient(URI.create(getValidUri()))).isNotEmpty(); - } - - @DisplayName("returns empty Optional server with invalid URI") - @Test - default void buildServerInvalidUri() { - assertThat(getUriHandler().buildServer(URI.create(getInvalidUri()))).isEmpty(); - } - - @DisplayName("buildServer throws NullPointerException with null uri") - @Test - default void buildServerNullUri() { - assertThatNullPointerException() - .isThrownBy(() -> getUriHandler().buildServer(null)) - .withMessage("uri must not be null"); - } - - @DisplayName("returns server with value URI") - @Test - default void buildServerValidUri() { - assertThat(getUriHandler().buildServer(URI.create(getValidUri()))).isNotEmpty(); - } - - String getInvalidUri(); - - UriHandler getUriHandler(); - - String getValidUri(); -} diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalUriHandler.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalUriHandler.java deleted file mode 100644 index 89c816d7a..000000000 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalUriHandler.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2015-2018 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.local; - -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import io.rsocket.uri.UriHandler; -import java.net.URI; -import java.util.Objects; -import java.util.Optional; - -/** - * An implementation of {@link UriHandler} that creates {@link LocalClientTransport}s and {@link - * LocalServerTransport}s. - */ -public final class LocalUriHandler implements UriHandler { - - private static final String SCHEME = "local"; - - @Override - public Optional buildClient(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (!SCHEME.equals(uri.getScheme())) { - return Optional.empty(); - } - - return Optional.of(LocalClientTransport.create(uri.getSchemeSpecificPart())); - } - - @Override - public Optional buildServer(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (!SCHEME.equals(uri.getScheme())) { - return Optional.empty(); - } - - return Optional.of(LocalServerTransport.create(uri.getSchemeSpecificPart())); - } -} diff --git a/rsocket-transport-local/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler b/rsocket-transport-local/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler deleted file mode 100644 index 6ff8ffb50..000000000 --- a/rsocket-transport-local/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler +++ /dev/null @@ -1,17 +0,0 @@ -# -# Copyright 2015-2018 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. -# - -io.rsocket.transport.local.LocalUriHandler diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriHandlerTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriHandlerTest.java deleted file mode 100644 index ed8e6cd1d..000000000 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriHandlerTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2015-2018 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.local; - -import io.rsocket.test.UriHandlerTest; -import io.rsocket.uri.UriHandler; - -final class LocalUriHandlerTest implements UriHandlerTest { - - @Override - public String getInvalidUri() { - return "http://test"; - } - - @Override - public UriHandler getUriHandler() { - return new LocalUriHandler(); - } - - @Override - public String getValidUri() { - return "local:test"; - } -} diff --git a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriTransportRegistryTest.java b/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriTransportRegistryTest.java deleted file mode 100644 index f6b5cda7e..000000000 --- a/rsocket-transport-local/src/test/java/io/rsocket/transport/local/LocalUriTransportRegistryTest.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2015-2018 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.local; - -import static org.assertj.core.api.Assertions.assertThat; - -import io.rsocket.uri.UriTransportRegistry; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -final class LocalUriTransportRegistryTest { - - @DisplayName("local URI returns LocalClientTransport") - @Test - void clientForUri() { - assertThat(UriTransportRegistry.clientForUri("local:test1")) - .isInstanceOf(LocalClientTransport.class); - } - - @DisplayName("non-local URI does not return LocalClientTransport") - @Test - void clientForUriInvalid() { - assertThat(UriTransportRegistry.clientForUri("http://localhost")) - .isNotInstanceOf(LocalClientTransport.class); - } - - @DisplayName("local URI returns LocalServerTransport") - @Test - void serverForUri() { - assertThat(UriTransportRegistry.serverForUri("local:test1")) - .isInstanceOf(LocalServerTransport.class); - } - - @DisplayName("non-local URI does not return LocalServerTransport") - @Test - void serverForUriInvalid() { - assertThat(UriTransportRegistry.serverForUri("http://localhost")) - .isNotInstanceOf(LocalServerTransport.class); - } -} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpUriHandler.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpUriHandler.java deleted file mode 100644 index d4ebd57b7..000000000 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpUriHandler.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2015-2018 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.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import io.rsocket.transport.netty.client.TcpClientTransport; -import io.rsocket.transport.netty.server.TcpServerTransport; -import io.rsocket.uri.UriHandler; -import java.net.URI; -import java.util.Objects; -import java.util.Optional; -import reactor.netty.tcp.TcpServer; - -/** - * An implementation of {@link UriHandler} that creates {@link TcpClientTransport}s and {@link - * TcpServerTransport}s. - */ -public final class TcpUriHandler implements UriHandler { - - private static final String SCHEME = "tcp"; - - @Override - public Optional buildClient(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (!SCHEME.equals(uri.getScheme())) { - return Optional.empty(); - } - - return Optional.of(TcpClientTransport.create(uri.getHost(), uri.getPort())); - } - - @Override - public Optional buildServer(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (!SCHEME.equals(uri.getScheme())) { - return Optional.empty(); - } - - return Optional.of( - TcpServerTransport.create(TcpServer.create().host(uri.getHost()).port(uri.getPort()))); - } -} diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketUriHandler.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketUriHandler.java deleted file mode 100644 index 6438c4e28..000000000 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketUriHandler.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright 2015-2018 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 io.rsocket.transport.netty.UriUtils.getPort; -import static io.rsocket.transport.netty.UriUtils.isSecure; - -import io.rsocket.transport.ClientTransport; -import io.rsocket.transport.ServerTransport; -import io.rsocket.transport.netty.client.WebsocketClientTransport; -import io.rsocket.transport.netty.server.WebsocketServerTransport; -import io.rsocket.uri.UriHandler; -import java.net.URI; -import java.util.Arrays; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -/** - * An implementation of {@link UriHandler} that creates {@link WebsocketClientTransport}s and {@link - * WebsocketServerTransport}s. - */ -public final class WebsocketUriHandler implements UriHandler { - - private static final List SCHEME = Arrays.asList("ws", "wss", "http", "https"); - - @Override - public Optional buildClient(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (SCHEME.stream().noneMatch(scheme -> scheme.equals(uri.getScheme()))) { - return Optional.empty(); - } - - return Optional.of(WebsocketClientTransport.create(uri)); - } - - @Override - public Optional buildServer(URI uri) { - Objects.requireNonNull(uri, "uri must not be null"); - - if (SCHEME.stream().noneMatch(scheme -> scheme.equals(uri.getScheme()))) { - return Optional.empty(); - } - - int port = isSecure(uri) ? getPort(uri, 443) : getPort(uri, 80); - - return Optional.of(WebsocketServerTransport.create(uri.getHost(), port)); - } -} diff --git a/rsocket-transport-netty/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler b/rsocket-transport-netty/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler deleted file mode 100644 index ec7ddcb80..000000000 --- a/rsocket-transport-netty/src/main/resources/META-INF/services/io.rsocket.uri.UriHandler +++ /dev/null @@ -1,18 +0,0 @@ -# -# Copyright 2015-2018 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. -# - -io.rsocket.transport.netty.TcpUriHandler -io.rsocket.transport.netty.WebsocketUriHandler diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpUriHandlerTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpUriHandlerTest.java deleted file mode 100644 index 25b443dd6..000000000 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpUriHandlerTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2015-2018 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.rsocket.test.UriHandlerTest; -import io.rsocket.uri.UriHandler; - -final class TcpUriHandlerTest implements UriHandlerTest { - - @Override - public String getInvalidUri() { - return "http://test"; - } - - @Override - public UriHandler getUriHandler() { - return new TcpUriHandler(); - } - - @Override - public String getValidUri() { - return "tcp://test:9898"; - } -} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpUriTransportRegistryTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpUriTransportRegistryTest.java deleted file mode 100644 index a71cc27f9..000000000 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpUriTransportRegistryTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2015-2018 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.rsocket.transport.netty.client.TcpClientTransport; -import io.rsocket.transport.netty.client.WebsocketClientTransport; -import io.rsocket.transport.netty.server.TcpServerTransport; -import io.rsocket.transport.netty.server.WebsocketServerTransport; -import io.rsocket.uri.UriTransportRegistry; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -final class TcpUriTransportRegistryTest { - - @DisplayName("non-tcp URI does not return TcpClientTransport") - @Test - void clientForUriInvalid() { - assertThat(UriTransportRegistry.clientForUri("amqp://localhost")) - .isNotInstanceOf(TcpClientTransport.class) - .isNotInstanceOf(WebsocketClientTransport.class); - } - - @DisplayName("tcp URI returns TcpClientTransport") - @Test - void clientForUriTcp() { - assertThat(UriTransportRegistry.clientForUri("tcp://test:9898")) - .isInstanceOf(TcpClientTransport.class); - } - - @DisplayName("non-tcp URI does not return TcpServerTransport") - @Test - void serverForUriInvalid() { - assertThat(UriTransportRegistry.serverForUri("amqp://localhost")) - .isNotInstanceOf(TcpServerTransport.class) - .isNotInstanceOf(WebsocketServerTransport.class); - } - - @DisplayName("tcp URI returns TcpServerTransport") - @Test - void serverForUriTcp() { - assertThat(UriTransportRegistry.serverForUri("tcp://test:9898")) - .isInstanceOf(TcpServerTransport.class); - } -} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriHandlerTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriHandlerTest.java deleted file mode 100644 index 72a700b0e..000000000 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriHandlerTest.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2015-2018 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.rsocket.test.UriHandlerTest; -import io.rsocket.uri.UriHandler; - -final class WebsocketUriHandlerTest implements UriHandlerTest { - - @Override - public String getInvalidUri() { - return "amqp://test"; - } - - @Override - public UriHandler getUriHandler() { - return new WebsocketUriHandler(); - } - - @Override - public String getValidUri() { - return "ws://test:9898"; - } -} diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriTransportRegistryTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriTransportRegistryTest.java deleted file mode 100644 index 5688f14ed..000000000 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketUriTransportRegistryTest.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2015-2018 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.rsocket.transport.netty.client.TcpClientTransport; -import io.rsocket.transport.netty.client.WebsocketClientTransport; -import io.rsocket.transport.netty.server.TcpServerTransport; -import io.rsocket.transport.netty.server.WebsocketServerTransport; -import io.rsocket.uri.UriTransportRegistry; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -final class WebsocketUriTransportRegistryTest { - - @DisplayName("non-ws URI does not return WebsocketClientTransport") - @Test - void clientForUriInvalid() { - assertThat(UriTransportRegistry.clientForUri("amqp://localhost")) - .isNotInstanceOf(TcpClientTransport.class) - .isNotInstanceOf(WebsocketClientTransport.class); - } - - @DisplayName("ws URI returns WebsocketClientTransport") - @Test - void clientForUriWebsocket() { - assertThat(UriTransportRegistry.clientForUri("ws://test:9898")) - .isInstanceOf(WebsocketClientTransport.class); - } - - @DisplayName("non-ws URI does not return WebsocketServerTransport") - @Test - void serverForUriInvalid() { - assertThat(UriTransportRegistry.serverForUri("amqp://localhost")) - .isNotInstanceOf(TcpServerTransport.class) - .isNotInstanceOf(WebsocketServerTransport.class); - } - - @DisplayName("ws URI returns WebsocketServerTransport") - @Test - void serverForUriWebsocket() { - assertThat(UriTransportRegistry.serverForUri("ws://test:9898")) - .isInstanceOf(WebsocketServerTransport.class); - } -} From 3a5a4c240d206db04b11658a3e81e9b9469f2ed8 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 24 Apr 2020 10:47:55 +0300 Subject: [PATCH 145/181] Fixes Payload#hasMetadata to strictly flow the flag in frame This PR ensures that Payload#hasMetadata will have identically value as it was encoded in frame decoded to Payload. Also, this PR ensures that during the encoding the current behavior of Payload#metadata \ Payload#sliceMetadata which always return a buffer (even though them hasMetadata is false) will not brake encoding process and the result flag in the encoded frame will be mirroring exactly what `Payload#hasMetadata` returned * fixes behaviour of flight-weights when it gets unreadable buffers ensures that ByteBufPayload is not accessible anymore when it has been released Signed-off-by: Oleh Dokuka * partial Signed-off-by: Oleh Dokuka * fixes incorrect request propagation Initially that issue was hidden because both sides uses limitRate which does prefetch 256 elements in advance so it is almost impossible to track underflow in request Signed-off-by: Oleh Dokuka * provides refactoring related to ensuring that hasMetadata is propagated correctly Right now there was observed a few issues related to that Payload with no metadata was incorrectly incoded so it results to hasMetadata true on the received side Also, optimized the API of Flyweights to ensure that we have common things in one place Signed-off-by: Oleh Dokuka * fixes failing tests related to changes of initialRequestN Signed-off-by: Oleh Dokuka * fixes compilation errors Signed-off-by: Oleh Dokuka * uncomment assertions related to proper metadata propagation Signed-off-by: Oleh Dokuka * moves payload releasing to codecs Signed-off-by: Oleh Dokuka --- .../core/DefaultConnectionSetupPayload.java | 4 +- .../io/rsocket/core/RSocketRequester.java | 52 +--- .../io/rsocket/core/RSocketResponder.java | 39 +-- .../fragmentation/FrameReassembler.java | 16 +- .../frame/DataAndMetadataFlyweight.java | 53 ++--- .../frame/ExtensionFrameFlyweight.java | 20 +- .../rsocket/frame/FragmentationFlyweight.java | 9 +- .../frame/KeepAliveFrameFlyweight.java | 2 +- .../io/rsocket/frame/LeaseFrameFlyweight.java | 13 +- .../frame/MetadataPushFrameFlyweight.java | 8 + .../rsocket/frame/PayloadFrameFlyweight.java | 74 +++--- .../frame/RequestChannelFrameFlyweight.java | 34 +-- .../RequestFireAndForgetFrameFlyweight.java | 13 +- .../io/rsocket/frame/RequestFlyweight.java | 25 +- .../rsocket/frame/RequestNFrameFlyweight.java | 15 +- .../frame/RequestResponseFrameFlyweight.java | 13 +- .../frame/RequestStreamFrameFlyweight.java | 51 ++-- .../io/rsocket/frame/SetupFrameFlyweight.java | 23 +- .../frame/decoder/DefaultPayloadDecoder.java | 25 +- .../frame/decoder/ZeroCopyPayloadDecoder.java | 11 +- .../java/io/rsocket/util/ByteBufPayload.java | 29 ++- .../io/rsocket/core/RSocketRequesterTest.java | 111 ++++++++- .../io/rsocket/core/RSocketResponderTest.java | 142 +++++++++-- .../java/io/rsocket/core/RSocketTest.java | 222 +++++++++--------- .../FragmentationIntegrationTest.java | 3 +- .../fragmentation/FrameFragmenterTest.java | 55 +++-- .../fragmentation/FrameReassemblerTest.java | 112 ++++----- .../ReassembleDuplexConnectionTest.java | 48 ++-- .../frame/DataAndMetadataFlyweightTest.java | 51 ---- .../frame/ExtensionFrameFlyweightTest.java | 2 +- .../rsocket/frame/PayloadFlyweightTest.java | 31 ++- .../rsocket/frame/RequestFlyweightTest.java | 47 ++-- .../io/rsocket/util/ByteBufPayloadTest.java | 64 +++++ .../io/rsocket/util/DefaultPayloadTest.java | 30 ++- .../main/java/io/rsocket/test/TestFrames.java | 4 +- .../java/io/rsocket/test/TransportTest.java | 10 +- 36 files changed, 872 insertions(+), 589 deletions(-) delete mode 100644 rsocket-core/src/test/java/io/rsocket/frame/DataAndMetadataFlyweightTest.java create mode 100644 rsocket-core/src/test/java/io/rsocket/util/ByteBufPayloadTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java index 8710aa61a..feeb5c481 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java @@ -17,6 +17,7 @@ package io.rsocket.core; import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.rsocket.ConnectionSetupPayload; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.SetupFrameFlyweight; @@ -40,7 +41,8 @@ public boolean hasMetadata() { @Override public ByteBuf sliceMetadata() { - return SetupFrameFlyweight.metadata(setupFrame); + final ByteBuf metadata = SetupFrameFlyweight.metadata(setupFrame); + return metadata == null ? Unpooled.EMPTY_BUFFER : metadata; } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 4d7d47f6a..42a6a524d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -212,13 +212,8 @@ private Mono handleFireAndForget(Payload payload) { return UnicastMonoEmpty.newInstance( () -> { ByteBuf requestFrame = - RequestFireAndForgetFrameFlyweight.encode( - allocator, - streamId, - false, - payload.hasMetadata() ? payload.sliceMetadata().retain() : null, - payload.sliceData().retain()); - payload.release(); + RequestFireAndForgetFrameFlyweight.encodeReleasingPayload( + allocator, streamId, payload); sendProcessor.onNext(requestFrame); }); @@ -245,13 +240,8 @@ private Mono handleRequestResponse(final Payload payload) { @Override public void doOnSubscribe() { final ByteBuf requestFrame = - RequestResponseFrameFlyweight.encode( - allocator, - streamId, - false, - payload.sliceMetadata().retain(), - payload.sliceData().retain()); - payload.release(); + RequestResponseFrameFlyweight.encodeReleasingPayload( + allocator, streamId, payload); sendProcessor.onNext(requestFrame); } @@ -302,16 +292,10 @@ private Flux handleRequestStream(final Payload payload) { public void accept(long n) { if (firstRequest && !receiver.isDisposed()) { firstRequest = false; - sendProcessor.onNext( - RequestStreamFrameFlyweight.encode( - allocator, - streamId, - false, - n, - payload.sliceMetadata().retain(), - payload.sliceData().retain())); if (!payloadReleasedFlag.getAndSet(true)) { - payload.release(); + sendProcessor.onNext( + RequestStreamFrameFlyweight.encodeReleasingPayload( + allocator, streamId, n, payload)); } } else if (contains(streamId) && !receiver.isDisposed()) { sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); @@ -400,10 +384,9 @@ protected void hookOnNext(Payload payload) { return; } final ByteBuf frame = - PayloadFrameFlyweight.encode(allocator, streamId, false, false, true, payload); + PayloadFrameFlyweight.encodeNextReleasingPayload(allocator, streamId, payload); sendProcessor.onNext(frame); - payload.release(); } @Override @@ -444,18 +427,10 @@ public void accept(long n) { .subscribe(upstreamSubscriber); if (!payloadReleasedFlag.getAndSet(true)) { ByteBuf frame = - RequestChannelFrameFlyweight.encode( - allocator, - streamId, - false, - false, - n, - initialPayload.sliceMetadata().retain(), - initialPayload.sliceData().retain()); + RequestChannelFrameFlyweight.encodeReleasingPayload( + allocator, streamId, false, n, initialPayload); sendProcessor.onNext(frame); - - initialPayload.release(); } } else { sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); @@ -497,8 +472,7 @@ private Mono handleMetadataPush(Payload payload) { return UnicastMonoEmpty.newInstance( () -> { ByteBuf metadataPushFrame = - MetadataPushFrameFlyweight.encode(allocator, payload.sliceMetadata().retain()); - payload.release(); + MetadataPushFrameFlyweight.encodeReleasingPayload(allocator, payload); sendProcessor.onNextPrioritized(metadataPushFrame); }); @@ -604,8 +578,8 @@ private void handleFrame(int streamId, FrameType type, ByteBuf frame) { { Subscription sender = senders.get(streamId); if (sender != null) { - int n = RequestNFrameFlyweight.requestN(frame); - sender.request(n >= Integer.MAX_VALUE ? Long.MAX_VALUE : n); + long n = RequestNFrameFlyweight.requestN(frame); + sender.request(n); } break; } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index f9c3d1c65..5aef7eed2 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -301,12 +301,12 @@ private void handleFrame(ByteBuf frame) { handleRequestN(streamId, frame); break; case REQUEST_STREAM: - int streamInitialRequestN = RequestStreamFrameFlyweight.initialRequestN(frame); + long streamInitialRequestN = RequestStreamFrameFlyweight.initialRequestN(frame); Payload streamPayload = payloadDecoder.apply(frame); handleStream(streamId, requestStream(streamPayload), streamInitialRequestN, null); break; case REQUEST_CHANNEL: - int channelInitialRequestN = RequestChannelFrameFlyweight.initialRequestN(frame); + long channelInitialRequestN = RequestChannelFrameFlyweight.initialRequestN(frame); Payload channelPayload = payloadDecoder.apply(frame); handleChannel(streamId, channelPayload, channelInitialRequestN); break; @@ -399,16 +399,9 @@ protected void hookOnNext(Payload payload) { return; } - ByteBuf byteBuf; - try { - byteBuf = PayloadFrameFlyweight.encodeNextComplete(allocator, streamId, payload); - } catch (Throwable t) { - payload.release(); - throw Exceptions.propagate(t); - } - - payload.release(); - + ByteBuf byteBuf = + PayloadFrameFlyweight.encodeNextCompleteReleasingPayload( + allocator, streamId, payload); sendProcessor.onNext(byteBuf); } @@ -437,14 +430,14 @@ protected void hookFinally(SignalType type) { private void handleStream( int streamId, Flux response, - int initialRequestN, + long initialRequestN, @Nullable UnicastProcessor requestChannel) { final BaseSubscriber subscriber = new BaseSubscriber() { @Override protected void hookOnSubscribe(Subscription s) { - s.request(initialRequestN >= Integer.MAX_VALUE ? Long.MAX_VALUE : initialRequestN); + s.request(initialRequestN); } @Override @@ -469,16 +462,8 @@ protected void hookOnNext(Payload payload) { return; } - ByteBuf byteBuf; - try { - byteBuf = PayloadFrameFlyweight.encodeNext(allocator, streamId, payload); - } catch (Throwable t) { - payload.release(); - throw Exceptions.propagate(t); - } - - payload.release(); - + ByteBuf byteBuf = + PayloadFrameFlyweight.encodeNextReleasingPayload(allocator, streamId, payload); sendProcessor.onNext(byteBuf); } @@ -523,7 +508,7 @@ protected void hookFinally(SignalType type) { .subscribe(subscriber); } - private void handleChannel(int streamId, Payload payload, int initialRequestN) { + private void handleChannel(int streamId, Payload payload, long initialRequestN) { UnicastProcessor frames = UnicastProcessor.create(); channelProcessors.put(streamId, frames); @@ -602,8 +587,8 @@ private void handleRequestN(int streamId, ByteBuf frame) { Subscription subscription = sendingSubscriptions.get(streamId); if (subscription != null) { - int n = RequestNFrameFlyweight.requestN(frame); - subscription.request(n >= Integer.MAX_VALUE ? Long.MAX_VALUE : n); + long n = RequestNFrameFlyweight.requestN(frame); + subscription.request(n); } } } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java index d8537ec1a..1a8d242b2 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java @@ -166,8 +166,8 @@ void handleFollowsFlag(ByteBuf frame, int streamId, FrameType frameType) { header = frame.copy(frame.readerIndex(), FrameHeaderFlyweight.size()); if (frameType == FrameType.REQUEST_CHANNEL || frameType == FrameType.REQUEST_STREAM) { - int i = RequestChannelFrameFlyweight.initialRequestN(frame); - header.writeInt(i); + long i = RequestChannelFrameFlyweight.initialRequestN(frame); + header.writeInt(i > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) i); } putHeader(streamId, header); } @@ -261,10 +261,16 @@ void reassembleFrame(ByteBuf frame, SynchronousSink sink) { private ByteBuf assembleFrameWithMetadata(ByteBuf frame, int streamId, ByteBuf header) { ByteBuf metadata; CompositeByteBuf cm = removeMetadata(streamId); - if (cm != null) { - metadata = cm.addComponents(true, PayloadFrameFlyweight.metadata(frame).retain()); + + ByteBuf decodedMetadata = PayloadFrameFlyweight.metadata(frame); + if (decodedMetadata != null) { + if (cm != null) { + metadata = cm.addComponents(true, decodedMetadata.retain()); + } else { + metadata = PayloadFrameFlyweight.metadata(frame).retain(); + } } else { - metadata = PayloadFrameFlyweight.metadata(frame).retain(); + metadata = cm != null ? cm : null; } ByteBuf data = assembleData(frame, streamId); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java index d910fe92f..ea54aa374 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java @@ -30,38 +30,35 @@ private static int decodeLength(final ByteBuf byteBuf) { return length; } - static ByteBuf encodeOnlyMetadata( - ByteBufAllocator allocator, final ByteBuf header, ByteBuf metadata) { - return allocator.compositeBuffer(2).addComponents(true, header, metadata); - } - - static ByteBuf encodeOnlyData(ByteBufAllocator allocator, final ByteBuf header, ByteBuf data) { - return allocator.compositeBuffer(2).addComponents(true, header, data); - } - static ByteBuf encode( - ByteBufAllocator allocator, final ByteBuf header, ByteBuf metadata, ByteBuf data) { + ByteBufAllocator allocator, + final ByteBuf header, + ByteBuf metadata, + boolean hasMetadata, + ByteBuf data) { - int length = metadata.readableBytes(); - encodeLength(header, length); - return allocator.compositeBuffer(3).addComponents(true, header, metadata, data); - } + final boolean addData = data != null && data.isReadable(); + final boolean addMetadata = hasMetadata && metadata.isReadable(); - static ByteBuf metadataWithoutMarking(ByteBuf byteBuf, boolean hasMetadata) { if (hasMetadata) { - int length = decodeLength(byteBuf); - return byteBuf.readSlice(length); + int length = metadata.readableBytes(); + encodeLength(header, length); + } + + if (addMetadata && addData) { + return allocator.compositeBuffer(3).addComponents(true, header, metadata, data); + } else if (addMetadata) { + return allocator.compositeBuffer(2).addComponents(true, header, metadata); + } else if (addData) { + return allocator.compositeBuffer(2).addComponents(true, header, data); } else { - return Unpooled.EMPTY_BUFFER; + return header; } } - static ByteBuf metadata(ByteBuf byteBuf, boolean hasMetadata) { - byteBuf.markReaderIndex(); - byteBuf.skipBytes(6); - ByteBuf metadata = metadataWithoutMarking(byteBuf, hasMetadata); - byteBuf.resetReaderIndex(); - return metadata; + static ByteBuf metadataWithoutMarking(ByteBuf byteBuf) { + int length = decodeLength(byteBuf); + return byteBuf.readSlice(length); } static ByteBuf dataWithoutMarking(ByteBuf byteBuf, boolean hasMetadata) { @@ -76,12 +73,4 @@ static ByteBuf dataWithoutMarking(ByteBuf byteBuf, boolean hasMetadata) { return Unpooled.EMPTY_BUFFER; } } - - static ByteBuf data(ByteBuf byteBuf, boolean hasMetadata) { - byteBuf.markReaderIndex(); - byteBuf.skipBytes(6); - ByteBuf data = dataWithoutMarking(byteBuf, hasMetadata); - byteBuf.resetReaderIndex(); - return data; - } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameFlyweight.java index df8b308e9..8cb01b08f 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameFlyweight.java @@ -14,21 +14,18 @@ public static ByteBuf encode( @Nullable ByteBuf metadata, ByteBuf data) { + final boolean hasMetadata = metadata != null; + int flags = FrameHeaderFlyweight.FLAGS_I; - if (metadata != null) { + if (hasMetadata) { flags |= FrameHeaderFlyweight.FLAGS_M; } - ByteBuf header = FrameHeaderFlyweight.encode(allocator, streamId, FrameType.EXT, flags); + final ByteBuf header = FrameHeaderFlyweight.encode(allocator, streamId, FrameType.EXT, flags); header.writeInt(extendedType); - if (data == null && metadata == null) { - return header; - } else if (metadata != null) { - return DataAndMetadataFlyweight.encode(allocator, header, metadata, data); - } else { - return DataAndMetadataFlyweight.encodeOnlyData(allocator, header, data); - } + + return DataAndMetadataFlyweight.encode(allocator, header, metadata, hasMetadata, data); } public static int extendedType(ByteBuf byteBuf) { @@ -56,10 +53,13 @@ public static ByteBuf metadata(ByteBuf byteBuf) { FrameHeaderFlyweight.ensureFrameType(FrameType.EXT, byteBuf); boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(byteBuf); + if (!hasMetadata) { + return null; + } byteBuf.markReaderIndex(); // Extended type byteBuf.skipBytes(FrameHeaderFlyweight.size() + Integer.BYTES); - ByteBuf metadata = DataAndMetadataFlyweight.metadataWithoutMarking(byteBuf, hasMetadata); + ByteBuf metadata = DataAndMetadataFlyweight.metadataWithoutMarking(byteBuf); byteBuf.resetReaderIndex(); return metadata; } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java index 06efeab6c..a91d52782 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java @@ -13,12 +13,7 @@ public static ByteBuf encode(final ByteBufAllocator allocator, ByteBuf header, B public static ByteBuf encode( final ByteBufAllocator allocator, ByteBuf header, @Nullable ByteBuf metadata, ByteBuf data) { - if (data == null && metadata == null) { - return header; - } else if (metadata != null) { - return DataAndMetadataFlyweight.encode(allocator, header, metadata, data); - } else { - return DataAndMetadataFlyweight.encodeOnlyData(allocator, header, data); - } + final boolean hasMetadata = metadata != null; + return DataAndMetadataFlyweight.encode(allocator, header, metadata, hasMetadata, data); } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/KeepAliveFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/KeepAliveFrameFlyweight.java index e4e6029b3..b591412a6 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/KeepAliveFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/KeepAliveFrameFlyweight.java @@ -29,7 +29,7 @@ public static ByteBuf encode( header.writeLong(lp); - return DataAndMetadataFlyweight.encodeOnlyData(allocator, header, data); + return DataAndMetadataFlyweight.encode(allocator, header, null, false, data); } public static boolean respondFlag(ByteBuf byteBuf) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java index 4676f4c9d..039c72886 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java @@ -13,21 +13,24 @@ public static ByteBuf encode( final int numRequests, @Nullable final ByteBuf metadata) { + final boolean hasMetadata = metadata != null; + final boolean addMetadata = hasMetadata && metadata.isReadable(); + int flags = 0; - if (metadata != null) { + if (hasMetadata) { flags |= FrameHeaderFlyweight.FLAGS_M; } - ByteBuf header = + final ByteBuf header = FrameHeaderFlyweight.encodeStreamZero(allocator, FrameType.LEASE, flags) .writeInt(ttl) .writeInt(numRequests); - if (metadata == null) { - return header; + if (addMetadata) { + return allocator.compositeBuffer(2).addComponents(true, header, metadata); } else { - return DataAndMetadataFlyweight.encodeOnlyMetadata(allocator, header, metadata); + return header; } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameFlyweight.java index d37b573ba..e3a9a47ba 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameFlyweight.java @@ -2,8 +2,16 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; public class MetadataPushFrameFlyweight { + + public static ByteBuf encodeReleasingPayload(ByteBufAllocator allocator, Payload payload) { + final ByteBuf metadata = payload.metadata().retain(); + payload.release(); + return encode(allocator, metadata); + } + public static ByteBuf encode(ByteBufAllocator allocator, ByteBuf metadata) { ByteBuf header = FrameHeaderFlyweight.encodeStreamZero( diff --git a/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java index 4f67d9c72..4c2ebdf6e 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java @@ -9,6 +9,33 @@ public class PayloadFrameFlyweight { private PayloadFrameFlyweight() {} + public static ByteBuf encodeNextReleasingPayload( + ByteBufAllocator allocator, int streamId, Payload payload) { + return encodeReleasingPayload(allocator, streamId, false, payload); + } + + public static ByteBuf encodeNextCompleteReleasingPayload( + ByteBufAllocator allocator, int streamId, Payload payload) { + + return encodeReleasingPayload(allocator, streamId, true, payload); + } + + static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, int streamId, boolean complete, Payload payload) { + + final boolean hasMetadata = payload.hasMetadata(); + final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; + final ByteBuf data = payload.data().retain(); + + payload.release(); + + return encode(allocator, streamId, false, complete, true, metadata, data); + } + + public static ByteBuf encodeComplete(ByteBufAllocator allocator, int streamId) { + return encode(allocator, streamId, false, true, false, null, null); + } + public static ByteBuf encode( ByteBufAllocator allocator, int streamId, @@ -21,53 +48,6 @@ public static ByteBuf encode( allocator, streamId, fragmentFollows, complete, next, 0, metadata, data); } - public static ByteBuf encode( - ByteBufAllocator allocator, - int streamId, - boolean fragmentFollows, - boolean complete, - boolean next, - Payload payload) { - return FLYWEIGHT.encode( - allocator, - streamId, - fragmentFollows, - complete, - next, - 0, - payload.hasMetadata() ? payload.metadata().retain() : null, - payload.data().retain()); - } - - public static ByteBuf encodeNextComplete( - ByteBufAllocator allocator, int streamId, Payload payload) { - return FLYWEIGHT.encode( - allocator, - streamId, - false, - true, - true, - 0, - payload.hasMetadata() ? payload.metadata().retain() : null, - payload.data().retain()); - } - - public static ByteBuf encodeNext(ByteBufAllocator allocator, int streamId, Payload payload) { - return FLYWEIGHT.encode( - allocator, - streamId, - false, - false, - true, - 0, - payload.hasMetadata() ? payload.metadata().retain() : null, - payload.data().retain()); - } - - public static ByteBuf encodeComplete(ByteBufAllocator allocator, int streamId) { - return FLYWEIGHT.encode(allocator, streamId, false, true, false, 0, null, null); - } - public static ByteBuf data(ByteBuf byteBuf) { return FLYWEIGHT.data(byteBuf); } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java index 06ddcda03..7c3cbb574 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java @@ -10,25 +10,20 @@ public class RequestChannelFrameFlyweight { private RequestChannelFrameFlyweight() {} - public static ByteBuf encode( + public static ByteBuf encodeReleasingPayload( ByteBufAllocator allocator, int streamId, - boolean fragmentFollows, boolean complete, - long requestN, + long initialRequestN, Payload payload) { - int reqN = requestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) requestN; + final boolean hasMetadata = payload.hasMetadata(); + final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; + final ByteBuf data = payload.data().retain(); - return FLYWEIGHT.encode( - allocator, - streamId, - fragmentFollows, - complete, - false, - reqN, - payload.metadata(), - payload.data()); + payload.release(); + + return encode(allocator, streamId, false, complete, initialRequestN, metadata, data); } public static ByteBuf encode( @@ -36,11 +31,15 @@ public static ByteBuf encode( int streamId, boolean fragmentFollows, boolean complete, - long requestN, + long initialRequestN, ByteBuf metadata, ByteBuf data) { - int reqN = requestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) requestN; + if (initialRequestN < 1) { + throw new IllegalArgumentException("request n is less than 1"); + } + + int reqN = initialRequestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) initialRequestN; return FLYWEIGHT.encode( allocator, streamId, fragmentFollows, complete, false, reqN, metadata, data); @@ -54,7 +53,8 @@ public static ByteBuf metadata(ByteBuf byteBuf) { return FLYWEIGHT.metadataWithRequestN(byteBuf); } - public static int initialRequestN(ByteBuf byteBuf) { - return FLYWEIGHT.initialRequestN(byteBuf); + public static long initialRequestN(ByteBuf byteBuf) { + int requestN = FLYWEIGHT.initialRequestN(byteBuf); + return requestN == Integer.MAX_VALUE ? Long.MAX_VALUE : requestN; } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java index 5f2d606e4..287f765f7 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java @@ -10,11 +10,16 @@ public class RequestFireAndForgetFrameFlyweight { private RequestFireAndForgetFrameFlyweight() {} - public static ByteBuf encode( - ByteBufAllocator allocator, int streamId, boolean fragmentFollows, Payload payload) { + public static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, int streamId, Payload payload) { + + final boolean hasMetadata = payload.hasMetadata(); + final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; + final ByteBuf data = payload.data().retain(); + + payload.release(); - return FLYWEIGHT.encode( - allocator, streamId, fragmentFollows, payload.metadata(), payload.data()); + return FLYWEIGHT.encode(allocator, streamId, false, metadata, data); } public static ByteBuf encode( diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestFlyweight.java index 98d862f36..15fac9f55 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestFlyweight.java @@ -29,9 +29,12 @@ ByteBuf encode( int requestN, @Nullable ByteBuf metadata, ByteBuf data) { + + final boolean hasMetadata = metadata != null; + int flags = 0; - if (metadata != null) { + if (hasMetadata) { flags |= FrameHeaderFlyweight.FLAGS_M; } @@ -47,19 +50,13 @@ ByteBuf encode( flags |= FrameHeaderFlyweight.FLAGS_N; } - ByteBuf header = FrameHeaderFlyweight.encode(allocator, streamId, frameType, flags); + final ByteBuf header = FrameHeaderFlyweight.encode(allocator, streamId, frameType, flags); if (requestN > 0) { header.writeInt(requestN); } - if (data == null && metadata == null) { - return header; - } else if (metadata != null) { - return DataAndMetadataFlyweight.encode(allocator, header, metadata, data); - } else { - return DataAndMetadataFlyweight.encodeOnlyData(allocator, header, data); - } + return DataAndMetadataFlyweight.encode(allocator, header, metadata, hasMetadata, data); } ByteBuf data(ByteBuf byteBuf) { @@ -73,9 +70,12 @@ ByteBuf data(ByteBuf byteBuf) { ByteBuf metadata(ByteBuf byteBuf) { boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(byteBuf); + if (!hasMetadata) { + return null; + } byteBuf.markReaderIndex(); byteBuf.skipBytes(FrameHeaderFlyweight.size()); - ByteBuf metadata = DataAndMetadataFlyweight.metadataWithoutMarking(byteBuf, hasMetadata); + ByteBuf metadata = DataAndMetadataFlyweight.metadataWithoutMarking(byteBuf); byteBuf.resetReaderIndex(); return metadata; } @@ -91,9 +91,12 @@ ByteBuf dataWithRequestN(ByteBuf byteBuf) { ByteBuf metadataWithRequestN(ByteBuf byteBuf) { boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(byteBuf); + if (!hasMetadata) { + return null; + } byteBuf.markReaderIndex(); byteBuf.skipBytes(FrameHeaderFlyweight.size() + Integer.BYTES); - ByteBuf metadata = DataAndMetadataFlyweight.metadataWithoutMarking(byteBuf, hasMetadata); + ByteBuf metadata = DataAndMetadataFlyweight.metadataWithoutMarking(byteBuf); byteBuf.resetReaderIndex(); return metadata; } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java index 5a4c4c273..fe2c752cf 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java @@ -8,26 +8,23 @@ private RequestNFrameFlyweight() {} public static ByteBuf encode( final ByteBufAllocator allocator, final int streamId, long requestN) { - int reqN = requestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) requestN; - return encode(allocator, streamId, reqN); - } - - public static ByteBuf encode(final ByteBufAllocator allocator, final int streamId, int requestN) { - ByteBuf header = FrameHeaderFlyweight.encode(allocator, streamId, FrameType.REQUEST_N, 0); if (requestN < 1) { throw new IllegalArgumentException("request n is less than 1"); } - return header.writeInt(requestN); + int reqN = requestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) requestN; + + ByteBuf header = FrameHeaderFlyweight.encode(allocator, streamId, FrameType.REQUEST_N, 0); + return header.writeInt(reqN); } - public static int requestN(ByteBuf byteBuf) { + public static long requestN(ByteBuf byteBuf) { FrameHeaderFlyweight.ensureFrameType(FrameType.REQUEST_N, byteBuf); byteBuf.markReaderIndex(); byteBuf.skipBytes(FrameHeaderFlyweight.size()); int i = byteBuf.readInt(); byteBuf.resetReaderIndex(); - return i; + return i == Integer.MAX_VALUE ? Long.MAX_VALUE : i; } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java index 2e06c9b82..3fbac27d2 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java @@ -10,9 +10,16 @@ public class RequestResponseFrameFlyweight { private RequestResponseFrameFlyweight() {} - public static ByteBuf encode( - ByteBufAllocator allocator, int streamId, boolean fragmentFollows, Payload payload) { - return encode(allocator, streamId, fragmentFollows, payload.metadata(), payload.data()); + public static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, int streamId, Payload payload) { + + final boolean hasMetadata = payload.hasMetadata(); + final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; + final ByteBuf data = payload.data().retain(); + + payload.release(); + + return encode(allocator, streamId, false, metadata, data); } public static ByteBuf encode( diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java index 171c41990..ff1435652 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java @@ -10,46 +10,34 @@ public class RequestStreamFrameFlyweight { private RequestStreamFrameFlyweight() {} - public static ByteBuf encode( - ByteBufAllocator allocator, - int streamId, - boolean fragmentFollows, - long requestN, - Payload payload) { - return encode( - allocator, streamId, fragmentFollows, requestN, payload.metadata(), payload.data()); - } + public static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, int streamId, long initialRequestN, Payload payload) { - public static ByteBuf encode( - ByteBufAllocator allocator, - int streamId, - boolean fragmentFollows, - int requestN, - Payload payload) { - return encode( - allocator, streamId, fragmentFollows, requestN, payload.metadata(), payload.data()); - } + final boolean hasMetadata = payload.hasMetadata(); + final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; + final ByteBuf data = payload.data().retain(); - public static ByteBuf encode( - ByteBufAllocator allocator, - int streamId, - boolean fragmentFollows, - long requestN, - ByteBuf metadata, - ByteBuf data) { - int reqN = requestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) requestN; - return encode(allocator, streamId, fragmentFollows, reqN, metadata, data); + payload.release(); + + return encode(allocator, streamId, false, initialRequestN, metadata, data); } public static ByteBuf encode( ByteBufAllocator allocator, int streamId, boolean fragmentFollows, - int requestN, + long initialRequestN, ByteBuf metadata, ByteBuf data) { + + if (initialRequestN < 1) { + throw new IllegalArgumentException("request n is less than 1"); + } + + int reqN = initialRequestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) initialRequestN; + return FLYWEIGHT.encode( - allocator, streamId, fragmentFollows, false, false, requestN, metadata, data); + allocator, streamId, fragmentFollows, false, false, reqN, metadata, data); } public static ByteBuf data(ByteBuf byteBuf) { @@ -60,7 +48,8 @@ public static ByteBuf metadata(ByteBuf byteBuf) { return FLYWEIGHT.metadataWithRequestN(byteBuf); } - public static int initialRequestN(ByteBuf byteBuf) { - return FLYWEIGHT.initialRequestN(byteBuf); + public static long initialRequestN(ByteBuf byteBuf) { + int requestN = FLYWEIGHT.initialRequestN(byteBuf); + return requestN == Integer.MAX_VALUE ? Long.MAX_VALUE : requestN; } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java index 9f92e715f..bfb73fe22 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java @@ -55,8 +55,9 @@ public static ByteBuf encode( final String dataMimeType, final Payload setupPayload) { - ByteBuf metadata = setupPayload.hasMetadata() ? setupPayload.sliceMetadata() : null; - ByteBuf data = setupPayload.sliceData(); + final ByteBuf data = setupPayload.sliceData(); + final boolean hasMetadata = setupPayload.hasMetadata(); + final ByteBuf metadata = hasMetadata ? setupPayload.sliceMetadata() : null; int flags = 0; @@ -68,11 +69,11 @@ public static ByteBuf encode( flags |= FLAGS_WILL_HONOR_LEASE; } - if (metadata != null) { + if (hasMetadata) { flags |= FrameHeaderFlyweight.FLAGS_M; } - ByteBuf header = FrameHeaderFlyweight.encodeStreamZero(allocator, FrameType.SETUP, flags); + final ByteBuf header = FrameHeaderFlyweight.encodeStreamZero(allocator, FrameType.SETUP, flags); header.writeInt(CURRENT_VERSION).writeInt(keepaliveInterval).writeInt(maxLifetime); @@ -91,13 +92,8 @@ public static ByteBuf encode( length = ByteBufUtil.utf8Bytes(dataMimeType); header.writeByte(length); ByteBufUtil.writeUtf8(header, dataMimeType); - if (data == null && metadata == null) { - return header; - } else if (metadata != null) { - return DataAndMetadataFlyweight.encode(allocator, header, metadata, data); - } else { - return DataAndMetadataFlyweight.encodeOnlyData(allocator, header, data); - } + + return DataAndMetadataFlyweight.encode(allocator, header, metadata, hasMetadata, data); } public static int version(ByteBuf byteBuf) { @@ -192,9 +188,12 @@ public static String dataMimeType(ByteBuf byteBuf) { public static ByteBuf metadata(ByteBuf byteBuf) { boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(byteBuf); + if (!hasMetadata) { + return null; + } byteBuf.markReaderIndex(); skipToPayload(byteBuf); - ByteBuf metadata = DataAndMetadataFlyweight.metadataWithoutMarking(byteBuf, hasMetadata); + ByteBuf metadata = DataAndMetadataFlyweight.metadataWithoutMarking(byteBuf); byteBuf.resetReaderIndex(); return metadata; } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java b/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java index 692dcb363..0a77e3820 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java @@ -3,8 +3,15 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.Payload; -import io.rsocket.frame.*; -import io.rsocket.util.ByteBufPayload; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.MetadataPushFrameFlyweight; +import io.rsocket.frame.PayloadFrameFlyweight; +import io.rsocket.frame.RequestChannelFrameFlyweight; +import io.rsocket.frame.RequestFireAndForgetFrameFlyweight; +import io.rsocket.frame.RequestResponseFrameFlyweight; +import io.rsocket.frame.RequestStreamFrameFlyweight; +import io.rsocket.util.DefaultPayload; import java.nio.ByteBuffer; /** Default Frame decoder that copies the frames contents for easy of use. */ @@ -45,14 +52,18 @@ public Payload apply(ByteBuf byteBuf) { throw new IllegalArgumentException("unsupported frame type: " + type); } - ByteBuffer metadata = ByteBuffer.allocateDirect(m.readableBytes()); ByteBuffer data = ByteBuffer.allocateDirect(d.readableBytes()); - data.put(d.nioBuffer()); data.flip(); - metadata.put(m.nioBuffer()); - metadata.flip(); - return ByteBufPayload.create(data, metadata); + if (m != null) { + ByteBuffer metadata = ByteBuffer.allocateDirect(m.readableBytes()); + metadata.put(m.nioBuffer()); + metadata.flip(); + + return DefaultPayload.create(data, metadata); + } + + return DefaultPayload.create(data); } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/decoder/ZeroCopyPayloadDecoder.java b/rsocket-core/src/main/java/io/rsocket/frame/decoder/ZeroCopyPayloadDecoder.java index 0b63590e8..c92f82428 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/decoder/ZeroCopyPayloadDecoder.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/decoder/ZeroCopyPayloadDecoder.java @@ -3,7 +3,14 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.Payload; -import io.rsocket.frame.*; +import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameType; +import io.rsocket.frame.MetadataPushFrameFlyweight; +import io.rsocket.frame.PayloadFrameFlyweight; +import io.rsocket.frame.RequestChannelFrameFlyweight; +import io.rsocket.frame.RequestFireAndForgetFrameFlyweight; +import io.rsocket.frame.RequestResponseFrameFlyweight; +import io.rsocket.frame.RequestStreamFrameFlyweight; import io.rsocket.util.ByteBufPayload; /** @@ -46,6 +53,6 @@ public Payload apply(ByteBuf byteBuf) { throw new IllegalArgumentException("unsupported frame type: " + type); } - return ByteBufPayload.create(d.retain(), m.retain()); + return ByteBufPayload.create(d.retain(), m != null ? m.retain() : null); } } diff --git a/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java b/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java index b91cf8ac6..f5d747f7f 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java @@ -21,6 +21,7 @@ import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.util.AbstractReferenceCounted; +import io.netty.util.IllegalReferenceCountException; import io.netty.util.Recycler; import io.netty.util.Recycler.Handle; import io.rsocket.Payload; @@ -112,9 +113,10 @@ public static Payload create(ByteBuf data) { public static Payload create(ByteBuf data, @Nullable ByteBuf metadata) { ByteBufPayload payload = RECYCLER.get(); - payload.setRefCnt(1); payload.data = data; payload.metadata = metadata; + // unsure data and metadata is set before refCnt change + payload.setRefCnt(1); return payload; } @@ -126,26 +128,31 @@ public static Payload create(Payload payload) { @Override public boolean hasMetadata() { + ensureAccessible(); return metadata != null; } @Override public ByteBuf sliceMetadata() { + ensureAccessible(); return metadata == null ? Unpooled.EMPTY_BUFFER : metadata.slice(); } @Override public ByteBuf data() { + ensureAccessible(); return data; } @Override public ByteBuf metadata() { + ensureAccessible(); return metadata == null ? Unpooled.EMPTY_BUFFER : metadata; } @Override public ByteBuf sliceData() { + ensureAccessible(); return data.slice(); } @@ -163,6 +170,7 @@ public ByteBufPayload retain(int increment) { @Override public ByteBufPayload touch() { + ensureAccessible(); data.touch(); if (metadata != null) { metadata.touch(); @@ -172,6 +180,7 @@ public ByteBufPayload touch() { @Override public ByteBufPayload touch(Object hint) { + ensureAccessible(); data.touch(hint); if (metadata != null) { metadata.touch(hint); @@ -189,4 +198,22 @@ protected void deallocate() { } handle.recycle(this); } + + /** + * Should be called by every method that tries to access the buffers content to check if the + * buffer was released before. + */ + void ensureAccessible() { + if (!isAccessible()) { + throw new IllegalReferenceCountException(0); + } + } + + /** + * Used internally by {@link ByteBufPayload#ensureAccessible()} to try to guard against using the + * buffer after it was released (best-effort). + */ + boolean isAccessible() { + return refCnt() != 0; + } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index 36abb14b4..bc19d8132 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -20,6 +20,7 @@ import static io.rsocket.frame.FrameHeaderFlyweight.frameType; import static io.rsocket.frame.FrameType.CANCEL; import static io.rsocket.frame.FrameType.REQUEST_CHANNEL; +import static io.rsocket.frame.FrameType.REQUEST_FNF; import static io.rsocket.frame.FrameType.REQUEST_RESPONSE; import static io.rsocket.frame.FrameType.REQUEST_STREAM; import static org.hamcrest.MatcherAssert.assertThat; @@ -35,6 +36,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCounted; import io.rsocket.Payload; @@ -74,7 +76,9 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.runners.model.Statement; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -141,7 +145,7 @@ protected void hookOnSubscribe(Subscription subscription) { ByteBuf f = sent.get(0); assertThat("initial frame", frameType(f), is(REQUEST_STREAM)); - assertThat("initial request n", RequestStreamFrameFlyweight.initialRequestN(f), is(5)); + assertThat("initial request n", RequestStreamFrameFlyweight.initialRequestN(f), is(5L)); assertThat("should be released", f.release(), is(true)); rule.assertHasNoLeaks(); } @@ -190,7 +194,8 @@ public void testHandleValidFrame() { int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); rule.connection.addToReceivedBuffer( - PayloadFrameFlyweight.encodeNext(rule.alloc(), streamId, EmptyPayload.INSTANCE)); + PayloadFrameFlyweight.encodeNextReleasingPayload( + rule.alloc(), streamId, EmptyPayload.INSTANCE)); verify(sub).onComplete(); Assertions.assertThat(rule.connection.getSent()).hasSize(1).allMatch(ReferenceCounted::release); @@ -328,7 +333,7 @@ protected void hookOnSubscribe(Subscription subscription) {} Assertions.assertThat(FrameHeaderFlyweight.frameType(initialFrame)).isEqualTo(REQUEST_CHANNEL); Assertions.assertThat(RequestChannelFrameFlyweight.initialRequestN(initialFrame)) - .isEqualTo(Integer.MAX_VALUE); + .isEqualTo(Long.MAX_VALUE); Assertions.assertThat( RequestChannelFrameFlyweight.data(initialFrame).toString(CharsetUtil.UTF_8)) .isEqualTo("0"); @@ -625,12 +630,110 @@ public void simpleOnDiscardRequestChannelTest2() { rule.assertHasNoLeaks(); } + @ParameterizedTest + @MethodSource("encodeDecodePayloadCases") + public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( + FrameType frameType, int framesCnt, int responsesCnt) { + ByteBufAllocator allocator = rule.alloc(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(responsesCnt); + TestPublisher testPublisher = TestPublisher.create(); + + Publisher response; + + switch (frameType) { + case REQUEST_FNF: + response = + testPublisher.mono().flatMap(p -> rule.socket.fireAndForget(p).then(Mono.empty())); + break; + case REQUEST_RESPONSE: + response = testPublisher.mono().flatMap(p -> rule.socket.requestResponse(p)); + break; + case REQUEST_STREAM: + response = testPublisher.mono().flatMapMany(p -> rule.socket.requestStream(p)); + break; + case REQUEST_CHANNEL: + response = rule.socket.requestChannel(testPublisher.flux()); + break; + default: + throw new UnsupportedOperationException("illegal case"); + } + + response.subscribe(assertSubscriber); + testPublisher.next(ByteBufPayload.create("d")); + + int streamId = rule.getStreamIdForRequestType(frameType); + + if (responsesCnt > 0) { + for (int i = 0; i < responsesCnt - 1; i++) { + rule.connection.addToReceivedBuffer( + PayloadFrameFlyweight.encode( + allocator, + streamId, + false, + false, + true, + null, + Unpooled.wrappedBuffer(("rd" + (i + 1)).getBytes()))); + } + + rule.connection.addToReceivedBuffer( + PayloadFrameFlyweight.encode( + allocator, + streamId, + false, + true, + true, + null, + Unpooled.wrappedBuffer(("rd" + responsesCnt).getBytes()))); + } + + if (framesCnt > 1) { + rule.connection.addToReceivedBuffer( + RequestNFrameFlyweight.encode(allocator, streamId, framesCnt)); + } + + for (int i = 1; i < framesCnt; i++) { + testPublisher.next(ByteBufPayload.create("d" + i)); + } + + Assertions.assertThat(rule.connection.getSent()) + .describedAs( + "Interaction Type :[%s]. Expected to observe %s frames sent", frameType, framesCnt) + .hasSize(framesCnt) + .allMatch(bb -> !FrameHeaderFlyweight.hasMetadata(bb)) + .allMatch(ByteBuf::release); + + Assertions.assertThat(assertSubscriber.isTerminated()) + .describedAs("Interaction Type :[%s]. Expected to be terminated", frameType) + .isTrue(); + + Assertions.assertThat(assertSubscriber.values()) + .describedAs( + "Interaction Type :[%s]. Expected to observe %s frames received", + frameType, responsesCnt) + .hasSize(responsesCnt) + .allMatch(p -> !p.hasMetadata()) + .allMatch(p -> p.release()); + + rule.assertHasNoLeaks(); + rule.connection.clearSendReceiveBuffers(); + } + + static Stream encodeDecodePayloadCases() { + return Stream.of( + Arguments.of(REQUEST_FNF, 1, 0), + Arguments.of(REQUEST_RESPONSE, 1, 1), + Arguments.of(REQUEST_STREAM, 1, 5), + Arguments.of(REQUEST_CHANNEL, 5, 5)); + } + public int sendRequestResponse(Publisher response) { Subscriber sub = TestSubscriber.create(); response.subscribe(sub); int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); rule.connection.addToReceivedBuffer( - PayloadFrameFlyweight.encodeNextComplete(rule.alloc(), streamId, EmptyPayload.INSTANCE)); + PayloadFrameFlyweight.encodeNextCompleteReleasingPayload( + rule.alloc(), streamId, EmptyPayload.INSTANCE)); verify(sub).onNext(any(Payload.class)); verify(sub).onComplete(); return streamId; diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index 05f9fd46e..78027aa3d 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -19,6 +19,8 @@ import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; import static io.rsocket.frame.FrameHeaderFlyweight.frameType; import static io.rsocket.frame.FrameType.REQUEST_CHANNEL; +import static io.rsocket.frame.FrameType.REQUEST_FNF; +import static io.rsocket.frame.FrameType.REQUEST_N; import static io.rsocket.frame.FrameType.REQUEST_RESPONSE; import static io.rsocket.frame.FrameType.REQUEST_STREAM; import static org.hamcrest.MatcherAssert.assertThat; @@ -45,6 +47,7 @@ import io.rsocket.frame.KeepAliveFrameFlyweight; import io.rsocket.frame.PayloadFrameFlyweight; import io.rsocket.frame.RequestChannelFrameFlyweight; +import io.rsocket.frame.RequestFireAndForgetFrameFlyweight; import io.rsocket.frame.RequestNFrameFlyweight; import io.rsocket.frame.RequestResponseFrameFlyweight; import io.rsocket.frame.RequestStreamFrameFlyweight; @@ -55,16 +58,21 @@ import io.rsocket.test.util.TestSubscriber; import io.rsocket.util.ByteBufPayload; import io.rsocket.util.DefaultPayload; +import io.rsocket.util.EmptyPayload; import java.util.Collection; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.runners.model.Statement; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -78,6 +86,7 @@ import reactor.core.publisher.Operators; import reactor.core.scheduler.Scheduler; import reactor.core.scheduler.Schedulers; +import reactor.test.publisher.TestPublisher; import reactor.test.util.RaceTestUtils; public class RSocketResponderTest { @@ -605,6 +614,108 @@ public Flux requestChannel(Publisher payloads) { rule.assertHasNoLeaks(); } + @ParameterizedTest + @MethodSource("encodeDecodePayloadCases") + public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( + FrameType frameType, int framesCnt, int responsesCnt) { + ByteBufAllocator allocator = rule.alloc(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(framesCnt); + TestPublisher testPublisher = TestPublisher.create(); + + rule.setAcceptingSocket( + new AbstractRSocket() { + @Override + public Mono fireAndForget(Payload payload) { + Mono.just(payload).subscribe(assertSubscriber); + return Mono.empty(); + } + + @Override + public Mono requestResponse(Payload payload) { + Mono.just(payload).subscribe(assertSubscriber); + return testPublisher.mono(); + } + + @Override + public Flux requestStream(Payload payload) { + Mono.just(payload).subscribe(assertSubscriber); + return testPublisher.flux(); + } + + @Override + public Flux requestChannel(Publisher payloads) { + payloads.subscribe(assertSubscriber); + return testPublisher.flux(); + } + }, + 1); + + rule.sendRequest(1, frameType, ByteBufPayload.create("d")); + + // if responses number is bigger than 1 we have to send one extra requestN + if (responsesCnt > 1) { + rule.connection.addToReceivedBuffer( + RequestNFrameFlyweight.encode(allocator, 1, responsesCnt - 1)); + } + + // respond with specific number of elements + for (int i = 0; i < responsesCnt; i++) { + testPublisher.next(ByteBufPayload.create("rd" + i)); + } + + // Listen to incoming frames. Valid for RequestChannel case only + if (framesCnt > 1) { + for (int i = 1; i < responsesCnt; i++) { + rule.connection.addToReceivedBuffer( + PayloadFrameFlyweight.encode( + allocator, + 1, + false, + false, + true, + null, + Unpooled.wrappedBuffer(("d" + (i + 1)).getBytes()))); + } + } + + if (responsesCnt > 0) { + Assertions.assertThat( + rule.connection.getSent().stream().filter(bb -> frameType(bb) != REQUEST_N)) + .describedAs( + "Interaction Type :[%s]. Expected to observe %s frames sent", frameType, responsesCnt) + .hasSize(responsesCnt) + .allMatch(bb -> !FrameHeaderFlyweight.hasMetadata(bb)); + } + + if (framesCnt > 1) { + Assertions.assertThat( + rule.connection.getSent().stream().filter(bb -> frameType(bb) == REQUEST_N)) + .describedAs( + "Interaction Type :[%s]. Expected to observe single RequestN(%s) frame", + frameType, framesCnt - 1) + .hasSize(1) + .first() + .matches(bb -> RequestNFrameFlyweight.requestN(bb) == (framesCnt - 1)); + } + + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); + + Assertions.assertThat(assertSubscriber.awaitAndAssertNextValueCount(framesCnt).values()) + .hasSize(framesCnt) + .allMatch(p -> !p.hasMetadata()) + .allMatch(ReferenceCounted::release); + + rule.assertHasNoLeaks(); + } + + static Stream encodeDecodePayloadCases() { + return Stream.of( + Arguments.of(REQUEST_FNF, 1, 0), + Arguments.of(REQUEST_RESPONSE, 1, 1), + Arguments.of(REQUEST_STREAM, 1, 5), + Arguments.of(REQUEST_CHANNEL, 5, 5)); + } + public static class ServerSocketRule extends AbstractSocketRule { private RSocket acceptingSocket; @@ -653,34 +764,31 @@ protected RSocketResponder newRSocket(LeaksTrackingByteBufAllocator allocator) { } private void sendRequest(int streamId, FrameType frameType) { + sendRequest(streamId, frameType, EmptyPayload.INSTANCE); + } + + private void sendRequest(int streamId, FrameType frameType, Payload payload) { ByteBuf request; switch (frameType) { case REQUEST_CHANNEL: request = - RequestChannelFrameFlyweight.encode( - allocator, - streamId, - false, - false, - prefetch, - Unpooled.EMPTY_BUFFER, - Unpooled.EMPTY_BUFFER); + RequestChannelFrameFlyweight.encodeReleasingPayload( + allocator, streamId, false, prefetch, payload); break; case REQUEST_STREAM: request = - RequestStreamFrameFlyweight.encode( - allocator, - streamId, - false, - prefetch, - Unpooled.EMPTY_BUFFER, - Unpooled.EMPTY_BUFFER); + RequestStreamFrameFlyweight.encodeReleasingPayload( + allocator, streamId, prefetch, payload); break; case REQUEST_RESPONSE: request = - RequestResponseFrameFlyweight.encode( - allocator, streamId, false, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); + RequestResponseFrameFlyweight.encodeReleasingPayload(allocator, streamId, payload); + break; + case REQUEST_FNF: + request = + RequestFireAndForgetFrameFlyweight.encodeReleasingPayload( + allocator, streamId, payload); break; default: throw new IllegalArgumentException("unsupported type: " + frameType); diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index f29f4409c..568f8eed6 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -183,231 +183,223 @@ public Flux requestChannel(Publisher payloads) { @Test public void requestChannelCase_StreamIsTerminatedAfterBothSidesSentCompletion1() { - TestPublisher outerPublisher = TestPublisher.create(); - AssertSubscriber outerAssertSubscriber = new AssertSubscriber<>(0); + TestPublisher requesterPublisher = TestPublisher.create(); + AssertSubscriber requesterSubscriber = new AssertSubscriber<>(0); - AssertSubscriber innerAssertSubscriber = new AssertSubscriber<>(0); - TestPublisher innerPublisher = TestPublisher.create(); + AssertSubscriber responderSubscriber = new AssertSubscriber<>(0); + TestPublisher responderPublisher = TestPublisher.create(); initRequestChannelCase( - outerPublisher, outerAssertSubscriber, innerPublisher, innerAssertSubscriber); + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); - nextFromOuterPublisher(outerPublisher, innerAssertSubscriber); + nextFromRequesterPublisher(requesterPublisher, responderSubscriber); - completeFromOuterPublisher(outerPublisher, innerAssertSubscriber); + completeFromRequesterPublisher(requesterPublisher, responderSubscriber); - nextFromInnerPublisher(innerPublisher, outerAssertSubscriber); + nextFromResponderPublisher(responderPublisher, requesterSubscriber); - completeFromInnerPublisher(innerPublisher, outerAssertSubscriber); + completeFromResponderPublisher(responderPublisher, requesterSubscriber); } @Test public void requestChannelCase_StreamIsTerminatedAfterBothSidesSentCompletion2() { - TestPublisher outerPublisher = TestPublisher.create(); - AssertSubscriber outerAssertSubscriber = new AssertSubscriber<>(0); + TestPublisher requesterPublisher = TestPublisher.create(); + AssertSubscriber requesterSubscriber = new AssertSubscriber<>(0); - AssertSubscriber innerAssertSubscriber = new AssertSubscriber<>(0); - TestPublisher innerPublisher = TestPublisher.create(); + AssertSubscriber responderSubscriber = new AssertSubscriber<>(0); + TestPublisher responderPublisher = TestPublisher.create(); initRequestChannelCase( - outerPublisher, outerAssertSubscriber, innerPublisher, innerAssertSubscriber); + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); - nextFromInnerPublisher(innerPublisher, outerAssertSubscriber); + nextFromResponderPublisher(responderPublisher, requesterSubscriber); - completeFromInnerPublisher(innerPublisher, outerAssertSubscriber); + completeFromResponderPublisher(responderPublisher, requesterSubscriber); - nextFromOuterPublisher(outerPublisher, innerAssertSubscriber); + nextFromRequesterPublisher(requesterPublisher, responderSubscriber); - completeFromOuterPublisher(outerPublisher, innerAssertSubscriber); + completeFromRequesterPublisher(requesterPublisher, responderSubscriber); } @Test public void requestChannelCase_CancellationFromResponderShouldLeaveStreamInHalfClosedStateWithNextCompletionPossibleFromRequester() { - TestPublisher outerPublisher = TestPublisher.create(); - AssertSubscriber outerAssertSubscriber = new AssertSubscriber<>(0); + TestPublisher requesterPublisher = TestPublisher.create(); + AssertSubscriber requesterSubscriber = new AssertSubscriber<>(0); - AssertSubscriber innerAssertSubscriber = new AssertSubscriber<>(0); - TestPublisher innerPublisher = TestPublisher.create(); + AssertSubscriber responderSubscriber = new AssertSubscriber<>(0); + TestPublisher responderPublisher = TestPublisher.create(); initRequestChannelCase( - outerPublisher, outerAssertSubscriber, innerPublisher, innerAssertSubscriber); + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); - nextFromOuterPublisher(outerPublisher, innerAssertSubscriber); + nextFromRequesterPublisher(requesterPublisher, responderSubscriber); - cancelFromInnerSubscriber(outerPublisher, innerAssertSubscriber); + cancelFromResponderSubscriber(requesterPublisher, responderSubscriber); - nextFromInnerPublisher(innerPublisher, outerAssertSubscriber); + nextFromResponderPublisher(responderPublisher, requesterSubscriber); - completeFromInnerPublisher(innerPublisher, outerAssertSubscriber); + completeFromResponderPublisher(responderPublisher, requesterSubscriber); } @Test public void requestChannelCase_CompletionFromRequesterShouldLeaveStreamInHalfClosedStateWithNextCancellationPossibleFromResponder() { - TestPublisher outerPublisher = TestPublisher.create(); - AssertSubscriber outerAssertSubscriber = new AssertSubscriber<>(0); + TestPublisher requesterPublisher = TestPublisher.create(); + AssertSubscriber requesterSubscriber = new AssertSubscriber<>(0); - AssertSubscriber innerAssertSubscriber = new AssertSubscriber<>(0); - TestPublisher innerPublisher = TestPublisher.create(); + AssertSubscriber responderSubscriber = new AssertSubscriber<>(0); + TestPublisher responderPublisher = TestPublisher.create(); initRequestChannelCase( - outerPublisher, outerAssertSubscriber, innerPublisher, innerAssertSubscriber); + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); - nextFromInnerPublisher(innerPublisher, outerAssertSubscriber); + nextFromResponderPublisher(responderPublisher, requesterSubscriber); - completeFromInnerPublisher(innerPublisher, outerAssertSubscriber); + completeFromResponderPublisher(responderPublisher, requesterSubscriber); - nextFromOuterPublisher(outerPublisher, innerAssertSubscriber); + nextFromRequesterPublisher(requesterPublisher, responderSubscriber); - cancelFromInnerSubscriber(outerPublisher, innerAssertSubscriber); + cancelFromResponderSubscriber(requesterPublisher, responderSubscriber); } @Test public void requestChannelCase_ensureThatRequesterSubscriberCancellationTerminatesStreamsOnBothSides() { - TestPublisher outerPublisher = TestPublisher.create(); - AssertSubscriber outerAssertSubscriber = new AssertSubscriber<>(0); + TestPublisher requesterPublisher = TestPublisher.create(); + AssertSubscriber requesterSubscriber = new AssertSubscriber<>(0); - AssertSubscriber innerAssertSubscriber = new AssertSubscriber<>(0); - TestPublisher innerPublisher = TestPublisher.create(); + AssertSubscriber responderSubscriber = new AssertSubscriber<>(0); + TestPublisher responderPublisher = TestPublisher.create(); initRequestChannelCase( - outerPublisher, outerAssertSubscriber, innerPublisher, innerAssertSubscriber); + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); - nextFromInnerPublisher(innerPublisher, outerAssertSubscriber); + nextFromResponderPublisher(responderPublisher, requesterSubscriber); - nextFromOuterPublisher(outerPublisher, innerAssertSubscriber); + nextFromRequesterPublisher(requesterPublisher, responderSubscriber); // ensures both sides are terminated - cancelFromOuterSubscriber( - outerPublisher, outerAssertSubscriber, innerPublisher, innerAssertSubscriber); + cancelFromRequesterSubscriber( + requesterPublisher, requesterSubscriber, responderPublisher, responderSubscriber); } void initRequestChannelCase( - TestPublisher outerPublisher, - AssertSubscriber outerAssertSubscriber, - TestPublisher innerPublisher, - AssertSubscriber innerAssertSubscriber) { + TestPublisher requesterPublisher, + AssertSubscriber requesterSubscriber, + TestPublisher responderPublisher, + AssertSubscriber responderSubscriber) { rule.setRequestAcceptor( new AbstractRSocket() { @Override public Flux requestChannel(Publisher payloads) { - payloads.subscribe(innerAssertSubscriber); - return innerPublisher.flux(); + payloads.subscribe(responderSubscriber); + return responderPublisher.flux(); } }); - rule.crs.requestChannel(outerPublisher).subscribe(outerAssertSubscriber); + rule.crs.requestChannel(requesterPublisher).subscribe(requesterSubscriber); - outerPublisher.assertWasSubscribed(); - outerAssertSubscriber.assertSubscribed(); + requesterPublisher.assertWasSubscribed(); + requesterSubscriber.assertSubscribed(); - innerAssertSubscriber.assertNotSubscribed(); - innerPublisher.assertWasNotSubscribed(); + responderSubscriber.assertNotSubscribed(); + responderPublisher.assertWasNotSubscribed(); // firstRequest - outerAssertSubscriber.request(1); - outerPublisher.assertMaxRequested(1); - outerPublisher.next(DefaultPayload.create("initialData", "initialMetadata")); + requesterSubscriber.request(1); + requesterPublisher.assertMaxRequested(1); + requesterPublisher.next(DefaultPayload.create("initialData", "initialMetadata")); - innerAssertSubscriber.assertSubscribed(); - innerPublisher.assertWasSubscribed(); + responderSubscriber.assertSubscribed(); + responderPublisher.assertWasSubscribed(); } - void nextFromOuterPublisher( - TestPublisher outerPublisher, AssertSubscriber innerAssertSubscriber) { + void nextFromRequesterPublisher( + TestPublisher requesterPublisher, AssertSubscriber responderSubscriber) { // ensures that outerUpstream and innerSubscriber is not terminated so the requestChannel - outerPublisher.assertSubscribers(1); - innerAssertSubscriber.assertNotTerminated(); + requesterPublisher.assertSubscribers(1); + responderSubscriber.assertNotTerminated(); - innerAssertSubscriber.request(6); - outerPublisher.next( + responderSubscriber.request(6); + requesterPublisher.next( DefaultPayload.create("d1", "m1"), DefaultPayload.create("d2"), DefaultPayload.create("d3", "m3"), DefaultPayload.create("d4"), DefaultPayload.create("d5", "m5")); - List innerPayloads = innerAssertSubscriber.awaitAndAssertNextValueCount(6).values(); + List innerPayloads = responderSubscriber.awaitAndAssertNextValueCount(6).values(); Assertions.assertThat(innerPayloads.stream().map(Payload::getDataUtf8)) .containsExactly("initialData", "d1", "d2", "d3", "d4", "d5"); - // fixme: incorrect behaviour of metadata encoding - // Assertions - // .assertThat(innerPayloads - // .stream() - // .map(Payload::hasMetadata) - // ) - // .containsExactly(true, true, false, true, false, true); + Assertions.assertThat(innerPayloads.stream().map(Payload::hasMetadata)) + .containsExactly(true, true, false, true, false, true); Assertions.assertThat(innerPayloads.stream().map(Payload::getMetadataUtf8)) .containsExactly("initialMetadata", "m1", "", "m3", "", "m5"); } - void completeFromOuterPublisher( - TestPublisher outerPublisher, AssertSubscriber innerAssertSubscriber) { + void completeFromRequesterPublisher( + TestPublisher requesterPublisher, AssertSubscriber responderSubscriber) { // ensures that after sending complete upstream part is closed - outerPublisher.complete(); - innerAssertSubscriber.assertTerminated(); - outerPublisher.assertNoSubscribers(); + requesterPublisher.complete(); + responderSubscriber.assertTerminated(); + requesterPublisher.assertNoSubscribers(); } - void cancelFromInnerSubscriber( - TestPublisher outerPublisher, AssertSubscriber innerAssertSubscriber) { + void cancelFromResponderSubscriber( + TestPublisher requesterPublisher, AssertSubscriber responderSubscriber) { // ensures that after sending complete upstream part is closed - innerAssertSubscriber.cancel(); - outerPublisher.assertWasCancelled(); - outerPublisher.assertNoSubscribers(); + responderSubscriber.cancel(); + requesterPublisher.assertWasCancelled(); + requesterPublisher.assertNoSubscribers(); } - void nextFromInnerPublisher( - TestPublisher innerPublisher, AssertSubscriber outerAssertSubscriber) { + void nextFromResponderPublisher( + TestPublisher responderPublisher, AssertSubscriber requesterSubscriber) { // ensures that downstream is not terminated so the requestChannel state is half-closed - innerPublisher.assertSubscribers(1); - outerAssertSubscriber.assertNotTerminated(); + responderPublisher.assertSubscribers(1); + requesterSubscriber.assertNotTerminated(); - // ensures innerPublisher can send messages and outerSubscriber can receive them - outerAssertSubscriber.request(5); - innerPublisher.next( + // ensures responderPublisher can send messages and outerSubscriber can receive them + requesterSubscriber.request(5); + responderPublisher.next( DefaultPayload.create("rd1", "rm1"), DefaultPayload.create("rd2"), DefaultPayload.create("rd3", "rm3"), DefaultPayload.create("rd4"), DefaultPayload.create("rd5", "rm5")); - List outerPayloads = outerAssertSubscriber.awaitAndAssertNextValueCount(5).values(); + List outerPayloads = requesterSubscriber.awaitAndAssertNextValueCount(5).values(); Assertions.assertThat(outerPayloads.stream().map(Payload::getDataUtf8)) .containsExactly("rd1", "rd2", "rd3", "rd4", "rd5"); - // fixme: incorrect behaviour of metadata encoding - // Assertions - // .assertThat(outerPayloads - // .stream() - // .map(Payload::hasMetadata) - // ) - // .containsExactly(true, false, true, false, true); + Assertions.assertThat(outerPayloads.stream().map(Payload::hasMetadata)) + .containsExactly(true, false, true, false, true); Assertions.assertThat(outerPayloads.stream().map(Payload::getMetadataUtf8)) .containsExactly("rm1", "", "rm3", "", "rm5"); } - void completeFromInnerPublisher( - TestPublisher innerPublisher, AssertSubscriber outerAssertSubscriber) { + void completeFromResponderPublisher( + TestPublisher responderPublisher, AssertSubscriber requesterSubscriber) { // ensures that after sending complete inner upstream is closed - innerPublisher.complete(); - outerAssertSubscriber.assertTerminated(); - innerPublisher.assertNoSubscribers(); + responderPublisher.complete(); + requesterSubscriber.assertTerminated(); + responderPublisher.assertNoSubscribers(); } - void cancelFromOuterSubscriber( - TestPublisher outerPublisher, - AssertSubscriber outerAssertSubscriber, - TestPublisher innerPublisher, - AssertSubscriber innerAssertSubscriber) { + void cancelFromRequesterSubscriber( + TestPublisher requesterPublisher, + AssertSubscriber requesterSubscriber, + TestPublisher responderPublisher, + AssertSubscriber responderSubscriber) { // ensures that after sending cancel the whole requestChannel is terminated - outerAssertSubscriber.cancel(); - innerPublisher.assertWasCancelled(); - innerPublisher.assertNoSubscribers(); + requesterSubscriber.cancel(); + // error should be propagated + responderSubscriber.assertTerminated(); + responderPublisher.assertWasCancelled(); + responderPublisher.assertNoSubscribers(); // ensures that cancellation is propagated to the actual upstream - outerPublisher.assertWasCancelled(); - outerPublisher.assertNoSubscribers(); + requesterPublisher.assertWasCancelled(); + requesterPublisher.assertNoSubscribers(); } public static class SocketRule extends ExternalResource { diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java index 984207936..a8569ef3b 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java @@ -28,7 +28,8 @@ public class FragmentationIntegrationTest { @Test void fragmentAndReassembleData() { ByteBuf frame = - PayloadFrameFlyweight.encodeNextComplete(allocator, 2, DefaultPayload.create(data)); + PayloadFrameFlyweight.encodeNextCompleteReleasingPayload( + allocator, 2, DefaultPayload.create(data)); System.out.println(FrameUtil.toString(frame)); frame.retain(); diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java index f5a013357..c6b1735e6 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java @@ -20,7 +20,6 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.rsocket.frame.*; -import io.rsocket.util.DefaultPayload; import java.util.concurrent.ThreadLocalRandom; import org.junit.Assert; import org.junit.jupiter.api.DisplayName; @@ -43,14 +42,17 @@ final class FrameFragmenterTest { @Test void testGettingData() { ByteBuf rr = - RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)); + RequestResponseFrameFlyweight.encode( + allocator, 1, true, null, Unpooled.wrappedBuffer(data)); ByteBuf fnf = - RequestFireAndForgetFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)); + RequestFireAndForgetFrameFlyweight.encode( + allocator, 1, true, null, Unpooled.wrappedBuffer(data)); ByteBuf rs = - RequestStreamFrameFlyweight.encode(allocator, 1, true, 1, DefaultPayload.create(data)); + RequestStreamFrameFlyweight.encode( + allocator, 1, true, 1, null, Unpooled.wrappedBuffer(data)); ByteBuf rc = RequestChannelFrameFlyweight.encode( - allocator, 1, true, false, 1, DefaultPayload.create(data)); + allocator, 1, true, false, 1, null, Unpooled.wrappedBuffer(data)); ByteBuf data = FrameFragmenter.getData(rr, FrameType.REQUEST_RESPONSE); Assert.assertEquals(data, Unpooled.wrappedBuffer(data)); @@ -73,16 +75,22 @@ void testGettingData() { void testGettingMetadata() { ByteBuf rr = RequestResponseFrameFlyweight.encode( - allocator, 1, true, DefaultPayload.create(data, metadata)); + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); ByteBuf fnf = RequestFireAndForgetFrameFlyweight.encode( - allocator, 1, true, DefaultPayload.create(data, metadata)); + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); ByteBuf rs = RequestStreamFrameFlyweight.encode( - allocator, 1, true, 1, DefaultPayload.create(data, metadata)); + allocator, 1, true, 1, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); ByteBuf rc = RequestChannelFrameFlyweight.encode( - allocator, 1, true, false, 1, DefaultPayload.create(data, metadata)); + allocator, + 1, + true, + false, + 1, + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data)); ByteBuf data = FrameFragmenter.getMetadata(rr, FrameType.REQUEST_RESPONSE); Assert.assertEquals(data, Unpooled.wrappedBuffer(metadata)); @@ -104,7 +112,8 @@ void testGettingMetadata() { @Test void returnEmptBufferWhenNoMetadataPresent() { ByteBuf rr = - RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)); + RequestResponseFrameFlyweight.encode( + allocator, 1, true, null, Unpooled.wrappedBuffer(data)); ByteBuf data = FrameFragmenter.getMetadata(rr, FrameType.REQUEST_RESPONSE); Assert.assertEquals(data, Unpooled.EMPTY_BUFFER); @@ -115,7 +124,8 @@ void returnEmptBufferWhenNoMetadataPresent() { @Test void encodeFirstFrameWithData() { ByteBuf rr = - RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)); + RequestResponseFrameFlyweight.encode( + allocator, 1, true, null, Unpooled.wrappedBuffer(data)); ByteBuf fragment = FrameFragmenter.encodeFirstFragment( @@ -144,7 +154,7 @@ void encodeFirstFrameWithData() { void encodeFirstWithDataChannel() { ByteBuf rc = RequestChannelFrameFlyweight.encode( - allocator, 1, true, false, 10, DefaultPayload.create(data)); + allocator, 1, true, false, 10, null, Unpooled.wrappedBuffer(data)); ByteBuf fragment = FrameFragmenter.encodeFirstFragment( @@ -173,7 +183,8 @@ void encodeFirstWithDataChannel() { @Test void encodeFirstWithDataStream() { ByteBuf rc = - RequestStreamFrameFlyweight.encode(allocator, 1, true, 50, DefaultPayload.create(data)); + RequestStreamFrameFlyweight.encode( + allocator, 1, true, 50, null, Unpooled.wrappedBuffer(data)); ByteBuf fragment = FrameFragmenter.encodeFirstFragment( @@ -203,10 +214,7 @@ void encodeFirstWithDataStream() { void encodeFirstFrameWithMetadata() { ByteBuf rr = RequestResponseFrameFlyweight.encode( - allocator, - 1, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))); + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER); ByteBuf fragment = FrameFragmenter.encodeFirstFragment( @@ -234,7 +242,7 @@ void encodeFirstFrameWithMetadata() { void encodeFirstWithDataAndMetadataStream() { ByteBuf rc = RequestStreamFrameFlyweight.encode( - allocator, 1, true, 50, DefaultPayload.create(data, metadata)); + allocator, 1, true, 50, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); ByteBuf fragment = FrameFragmenter.encodeFirstFragment( @@ -266,7 +274,8 @@ void encodeFirstWithDataAndMetadataStream() { @Test void fragmentData() { ByteBuf rr = - RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)); + RequestResponseFrameFlyweight.encode( + allocator, 1, true, null, Unpooled.wrappedBuffer(data)); Publisher fragments = FrameFragmenter.fragmentFrame(allocator, 1024, rr, FrameType.REQUEST_RESPONSE, false); @@ -293,11 +302,7 @@ void fragmentData() { void fragmentMetadata() { ByteBuf rr = RequestStreamFrameFlyweight.encode( - allocator, - 1, - true, - 10, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))); + allocator, 1, true, 10, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER); Publisher fragments = FrameFragmenter.fragmentFrame(allocator, 1024, rr, FrameType.REQUEST_STREAM, false); @@ -324,7 +329,7 @@ void fragmentMetadata() { void fragmentDataAndMetadata() { ByteBuf rr = RequestResponseFrameFlyweight.encode( - allocator, 1, true, DefaultPayload.create(data, metadata)); + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); Publisher fragments = FrameFragmenter.fragmentFrame(allocator, 1024, rr, FrameType.REQUEST_RESPONSE, false); diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java index 6e0d0dc1b..13632165b 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java @@ -22,7 +22,6 @@ import io.netty.buffer.Unpooled; import io.netty.util.ReferenceCountUtil; import io.rsocket.frame.*; -import io.rsocket.util.DefaultPayload; import java.util.Arrays; import java.util.List; import java.util.concurrent.ThreadLocalRandom; @@ -48,15 +47,16 @@ final class FrameReassemblerTest { void reassembleData() { List byteBufs = Arrays.asList( - RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)), + RequestResponseFrameFlyweight.encode( + allocator, 1, true, null, Unpooled.wrappedBuffer(data)), PayloadFrameFlyweight.encode( - allocator, 1, true, false, true, DefaultPayload.create(data)), + allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), PayloadFrameFlyweight.encode( - allocator, 1, true, false, true, DefaultPayload.create(data)), + allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), PayloadFrameFlyweight.encode( - allocator, 1, true, false, true, DefaultPayload.create(data)), + allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), PayloadFrameFlyweight.encode( - allocator, 1, false, false, true, DefaultPayload.create(data))); + allocator, 1, false, false, true, null, Unpooled.wrappedBuffer(data))); FrameReassembler reassembler = new FrameReassembler(allocator); @@ -88,7 +88,8 @@ void reassembleData() { void passthrough() { List byteBufs = Arrays.asList( - RequestResponseFrameFlyweight.encode(allocator, 1, false, DefaultPayload.create(data))); + RequestResponseFrameFlyweight.encode( + allocator, 1, false, null, Unpooled.wrappedBuffer(data))); FrameReassembler reassembler = new FrameReassembler(allocator); @@ -115,38 +116,39 @@ void reassembleMetadata() { List byteBufs = Arrays.asList( RequestResponseFrameFlyweight.encode( - allocator, - 1, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, false, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata)))); + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER)); FrameReassembler reassembler = new FrameReassembler(allocator); @@ -184,35 +186,40 @@ void reassembleMetadataChannel() { true, false, 100, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, false, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata)))); + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER)); FrameReassembler reassembler = new FrameReassembler(allocator); @@ -249,39 +256,39 @@ void reassembleMetadataStream() { List byteBufs = Arrays.asList( RequestStreamFrameFlyweight.encode( - allocator, - 1, - true, - 250, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + allocator, 1, true, 250, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, false, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata)))); + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER)); FrameReassembler reassembler = new FrameReassembler(allocator); @@ -319,34 +326,33 @@ void reassembleMetadataAndData() { List byteBufs = Arrays.asList( RequestResponseFrameFlyweight.encode( - allocator, - 1, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create( - Unpooled.wrappedBuffer(data), Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data)), PayloadFrameFlyweight.encode( - allocator, 1, false, false, true, DefaultPayload.create(data))); + allocator, 1, false, false, true, null, Unpooled.wrappedBuffer(data))); FrameReassembler reassembler = new FrameReassembler(allocator); @@ -387,32 +393,31 @@ public void cancelBeforeAssembling() { List byteBufs = Arrays.asList( RequestResponseFrameFlyweight.encode( - allocator, - 1, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create( - Unpooled.wrappedBuffer(data), Unpooled.wrappedBuffer(metadata)))); + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data))); FrameReassembler reassembler = new FrameReassembler(allocator); Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame).blockLast(); @@ -436,32 +441,31 @@ public void dispose() { List byteBufs = Arrays.asList( RequestResponseFrameFlyweight.encode( - allocator, - 1, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create( - Unpooled.wrappedBuffer(data), Unpooled.wrappedBuffer(metadata)))); + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data))); FrameReassembler reassembler = new FrameReassembler(allocator); Flux.fromIterable(byteBufs).handle(reassembler::reassembleFrame).blockLast(); diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java index b21a7c9da..c5abce339 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java @@ -30,7 +30,6 @@ import io.rsocket.frame.FrameType; import io.rsocket.frame.PayloadFrameFlyweight; import io.rsocket.frame.RequestResponseFrameFlyweight; -import io.rsocket.util.DefaultPayload; import java.util.Arrays; import java.util.List; import java.util.concurrent.ThreadLocalRandom; @@ -59,15 +58,16 @@ final class ReassembleDuplexConnectionTest { void reassembleData() { List byteBufs = Arrays.asList( - RequestResponseFrameFlyweight.encode(allocator, 1, true, DefaultPayload.create(data)), + RequestResponseFrameFlyweight.encode( + allocator, 1, true, null, Unpooled.wrappedBuffer(data)), PayloadFrameFlyweight.encode( - allocator, 1, true, false, true, DefaultPayload.create(data)), + allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), PayloadFrameFlyweight.encode( - allocator, 1, true, false, true, DefaultPayload.create(data)), + allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), PayloadFrameFlyweight.encode( - allocator, 1, true, false, true, DefaultPayload.create(data)), + allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), PayloadFrameFlyweight.encode( - allocator, 1, false, false, true, DefaultPayload.create(data))); + allocator, 1, false, false, true, null, Unpooled.wrappedBuffer(data))); CompositeByteBuf data = allocator @@ -99,38 +99,39 @@ void reassembleMetadata() { List byteBufs = Arrays.asList( RequestResponseFrameFlyweight.encode( - allocator, - 1, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, false, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata)))); + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER)); CompositeByteBuf metadata = allocator @@ -164,34 +165,33 @@ void reassembleMetadataAndData() { List byteBufs = Arrays.asList( RequestResponseFrameFlyweight.encode( - allocator, - 1, - true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create(Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.EMPTY_BUFFER), PayloadFrameFlyweight.encode( allocator, 1, true, false, true, - DefaultPayload.create( - Unpooled.wrappedBuffer(data), Unpooled.wrappedBuffer(metadata))), + Unpooled.wrappedBuffer(metadata), + Unpooled.wrappedBuffer(data)), PayloadFrameFlyweight.encode( - allocator, 1, false, false, true, DefaultPayload.create(data))); + allocator, 1, false, false, true, null, Unpooled.wrappedBuffer(data))); CompositeByteBuf data = allocator @@ -230,7 +230,7 @@ void reassembleMetadataAndData() { void reassembleNonFragment() { ByteBuf encode = RequestResponseFrameFlyweight.encode( - allocator, 1, false, DefaultPayload.create(Unpooled.wrappedBuffer(data))); + allocator, 1, false, null, Unpooled.wrappedBuffer(data)); when(delegate.receive()).thenReturn(Flux.just(encode)); when(delegate.onClose()).thenReturn(Mono.never()); diff --git a/rsocket-core/src/test/java/io/rsocket/frame/DataAndMetadataFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/DataAndMetadataFlyweightTest.java deleted file mode 100644 index 6f9113d73..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/DataAndMetadataFlyweightTest.java +++ /dev/null @@ -1,51 +0,0 @@ -package io.rsocket.frame; - -import io.netty.buffer.*; -import org.junit.jupiter.api.Test; - -class DataAndMetadataFlyweightTest { - @Test - void testEncodeData() { - ByteBuf header = FrameHeaderFlyweight.encode(ByteBufAllocator.DEFAULT, 1, FrameType.PAYLOAD, 0); - ByteBuf data = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "_I'm data_"); - ByteBuf frame = DataAndMetadataFlyweight.encodeOnlyData(ByteBufAllocator.DEFAULT, header, data); - ByteBuf d = DataAndMetadataFlyweight.data(frame, false); - String s = ByteBufUtil.prettyHexDump(d); - System.out.println(s); - } - - @Test - void testEncodeMetadata() { - ByteBuf header = FrameHeaderFlyweight.encode(ByteBufAllocator.DEFAULT, 1, FrameType.PAYLOAD, 0); - ByteBuf data = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "_I'm metadata_"); - ByteBuf frame = - DataAndMetadataFlyweight.encodeOnlyMetadata(ByteBufAllocator.DEFAULT, header, data); - ByteBuf d = DataAndMetadataFlyweight.data(frame, false); - String s = ByteBufUtil.prettyHexDump(d); - System.out.println(s); - } - - @Test - void testEncodeDataAndMetadata() { - ByteBuf header = - FrameHeaderFlyweight.encode(ByteBufAllocator.DEFAULT, 1, FrameType.REQUEST_RESPONSE, 0); - ByteBuf data = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "_I'm data_"); - ByteBuf metadata = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "_I'm metadata_"); - ByteBuf frame = - DataAndMetadataFlyweight.encode(ByteBufAllocator.DEFAULT, header, metadata, data); - ByteBuf m = DataAndMetadataFlyweight.metadata(frame, true); - String s = ByteBufUtil.prettyHexDump(m); - System.out.println(s); - FrameType frameType = FrameHeaderFlyweight.frameType(frame); - System.out.println(frameType); - - for (int i = 0; i < 10_000_000; i++) { - ByteBuf d1 = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "_I'm data_"); - ByteBuf m1 = ByteBufUtil.writeUtf8(ByteBufAllocator.DEFAULT, "_I'm metadata_"); - ByteBuf h1 = - FrameHeaderFlyweight.encode(ByteBufAllocator.DEFAULT, 1, FrameType.REQUEST_RESPONSE, 0); - ByteBuf f1 = DataAndMetadataFlyweight.encode(ByteBufAllocator.DEFAULT, h1, m1, d1); - f1.release(); - } - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameFlyweightTest.java index e337d4332..eea72c03e 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameFlyweightTest.java @@ -35,7 +35,7 @@ void extensionData() { Assertions.assertFalse(FrameHeaderFlyweight.hasMetadata(extension)); Assertions.assertEquals(extendedType, ExtensionFrameFlyweight.extendedType(extension)); - Assertions.assertEquals(0, ExtensionFrameFlyweight.metadata(extension).readableBytes()); + Assertions.assertNull(ExtensionFrameFlyweight.metadata(extension)); Assertions.assertEquals(data, ExtensionFrameFlyweight.data(extension)); extension.release(); } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/PayloadFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/PayloadFlyweightTest.java index 9ef89326a..439d23c15 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/PayloadFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/PayloadFlyweightTest.java @@ -15,7 +15,8 @@ public class PayloadFlyweightTest { void nextCompleteDataMetadata() { Payload payload = DefaultPayload.create("d", "md"); ByteBuf nextComplete = - PayloadFrameFlyweight.encodeNextComplete(ByteBufAllocator.DEFAULT, 1, payload); + PayloadFrameFlyweight.encodeNextCompleteReleasingPayload( + ByteBufAllocator.DEFAULT, 1, payload); String data = PayloadFrameFlyweight.data(nextComplete).toString(StandardCharsets.UTF_8); String metadata = PayloadFrameFlyweight.metadata(nextComplete).toString(StandardCharsets.UTF_8); Assertions.assertEquals("d", data); @@ -27,11 +28,12 @@ void nextCompleteDataMetadata() { void nextCompleteData() { Payload payload = DefaultPayload.create("d"); ByteBuf nextComplete = - PayloadFrameFlyweight.encodeNextComplete(ByteBufAllocator.DEFAULT, 1, payload); + PayloadFrameFlyweight.encodeNextCompleteReleasingPayload( + ByteBufAllocator.DEFAULT, 1, payload); String data = PayloadFrameFlyweight.data(nextComplete).toString(StandardCharsets.UTF_8); ByteBuf metadata = PayloadFrameFlyweight.metadata(nextComplete); Assertions.assertEquals("d", data); - Assertions.assertTrue(metadata.readableBytes() == 0); + Assertions.assertNull(metadata); nextComplete.release(); } @@ -42,7 +44,8 @@ void nextCompleteMetaData() { Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer("md".getBytes(StandardCharsets.UTF_8))); ByteBuf nextComplete = - PayloadFrameFlyweight.encodeNextComplete(ByteBufAllocator.DEFAULT, 1, payload); + PayloadFrameFlyweight.encodeNextCompleteReleasingPayload( + ByteBufAllocator.DEFAULT, 1, payload); ByteBuf data = PayloadFrameFlyweight.data(nextComplete); String metadata = PayloadFrameFlyweight.metadata(nextComplete).toString(StandardCharsets.UTF_8); Assertions.assertTrue(data.readableBytes() == 0); @@ -53,7 +56,8 @@ void nextCompleteMetaData() { @Test void nextDataMetadata() { Payload payload = DefaultPayload.create("d", "md"); - ByteBuf next = PayloadFrameFlyweight.encodeNext(ByteBufAllocator.DEFAULT, 1, payload); + ByteBuf next = + PayloadFrameFlyweight.encodeNextReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); String data = PayloadFrameFlyweight.data(next).toString(StandardCharsets.UTF_8); String metadata = PayloadFrameFlyweight.metadata(next).toString(StandardCharsets.UTF_8); Assertions.assertEquals("d", data); @@ -64,11 +68,24 @@ void nextDataMetadata() { @Test void nextData() { Payload payload = DefaultPayload.create("d"); - ByteBuf next = PayloadFrameFlyweight.encodeNext(ByteBufAllocator.DEFAULT, 1, payload); + ByteBuf next = + PayloadFrameFlyweight.encodeNextReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); String data = PayloadFrameFlyweight.data(next).toString(StandardCharsets.UTF_8); ByteBuf metadata = PayloadFrameFlyweight.metadata(next); Assertions.assertEquals("d", data); - Assertions.assertTrue(metadata.readableBytes() == 0); + Assertions.assertNull(metadata); + next.release(); + } + + @Test + void nextDataEmptyMetadata() { + Payload payload = DefaultPayload.create("d".getBytes(), new byte[0]); + ByteBuf next = + PayloadFrameFlyweight.encodeNextReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); + String data = PayloadFrameFlyweight.data(next).toString(StandardCharsets.UTF_8); + ByteBuf metadata = PayloadFrameFlyweight.metadata(next); + Assertions.assertEquals("d", data); + Assertions.assertEquals(metadata.readableBytes(), 0); next.release(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/RequestFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/RequestFlyweightTest.java index 9acec2c81..c19d4e1f4 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/RequestFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/RequestFlyweightTest.java @@ -22,8 +22,15 @@ void testEncoding() { Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); frame = FrameLengthFlyweight.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); - - assertEquals("000010000000011900000000010000026d6464", ByteBufUtil.hexDump(frame)); + // Encoded FrameLength⌍ ⌌ Encoded Headers + // | | ⌌ Encoded Request(1) + // | | | ⌌Encoded Metadata Length + // | | | | ⌌Encoded Metadata + // | | | | | ⌌Encoded Data + // __|________|_________|______|____|___| + // ↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓↓ + String expected = "000010000000011900000000010000026d6464"; + assertEquals(expected, ByteBufUtil.hexDump(frame)); frame.release(); } @@ -39,8 +46,14 @@ void testEncodingWithEmptyMetadata() { Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); frame = FrameLengthFlyweight.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); - - assertEquals("00000e0000000119000000000100000064", ByteBufUtil.hexDump(frame)); + // Encoded FrameLength⌍ ⌌ Encoded Headers + // | | ⌌ Encoded Request(1) + // | | | ⌌Encoded Metadata Length (0) + // | | | | ⌌Encoded Data + // __|________|_________|_______|___| + // ↓ ↓↓ ↓↓ ↓↓ ↓↓↓ + String expected = "00000e0000000119000000000100000064"; + assertEquals(expected, ByteBufUtil.hexDump(frame)); frame.release(); } @@ -57,7 +70,13 @@ void testEncodingWithNullMetadata() { frame = FrameLengthFlyweight.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); - assertEquals("00000b0000000118000000000164", ByteBufUtil.hexDump(frame)); + // Encoded FrameLength⌍ ⌌ Encoded Headers + // | | ⌌ Encoded Request(1) + // | | | ⌌Encoded Data + // __|________|_________|_____| + // ↓<-> ↓↓ <-> ↓↓ <-> ↓↓↓ + String expected = "00000b0000000118000000000164"; + assertEquals(expected, ByteBufUtil.hexDump(frame)); frame.release(); } @@ -96,7 +115,7 @@ void requestResponseData() { assertFalse(FrameHeaderFlyweight.hasMetadata(request)); assertEquals("d", data); - assertTrue(metadata.readableBytes() == 0); + assertNull(metadata); request.release(); } @@ -131,13 +150,13 @@ void requestStreamDataMetadata() { Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - int actualRequest = RequestStreamFrameFlyweight.initialRequestN(request); + long actualRequest = RequestStreamFrameFlyweight.initialRequestN(request); String data = RequestStreamFrameFlyweight.data(request).toString(StandardCharsets.UTF_8); String metadata = RequestStreamFrameFlyweight.metadata(request).toString(StandardCharsets.UTF_8); assertTrue(FrameHeaderFlyweight.hasMetadata(request)); - assertEquals(Integer.MAX_VALUE, actualRequest); + assertEquals(Long.MAX_VALUE, actualRequest); assertEquals("md", metadata); assertEquals("d", data); request.release(); @@ -154,13 +173,13 @@ void requestStreamData() { null, Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - int actualRequest = RequestStreamFrameFlyweight.initialRequestN(request); + long actualRequest = RequestStreamFrameFlyweight.initialRequestN(request); String data = RequestStreamFrameFlyweight.data(request).toString(StandardCharsets.UTF_8); ByteBuf metadata = RequestStreamFrameFlyweight.metadata(request); assertFalse(FrameHeaderFlyweight.hasMetadata(request)); - assertEquals(42, actualRequest); - assertTrue(metadata.readableBytes() == 0); + assertEquals(42L, actualRequest); + assertNull(metadata); assertEquals("d", data); request.release(); } @@ -176,13 +195,13 @@ void requestStreamMetadata() { Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), Unpooled.EMPTY_BUFFER); - int actualRequest = RequestStreamFrameFlyweight.initialRequestN(request); + long actualRequest = RequestStreamFrameFlyweight.initialRequestN(request); ByteBuf data = RequestStreamFrameFlyweight.data(request); String metadata = RequestStreamFrameFlyweight.metadata(request).toString(StandardCharsets.UTF_8); assertTrue(FrameHeaderFlyweight.hasMetadata(request)); - assertEquals(42, actualRequest); + assertEquals(42L, actualRequest); assertTrue(data.readableBytes() == 0); assertEquals("md", metadata); request.release(); @@ -223,7 +242,7 @@ void requestFnfData() { assertFalse(FrameHeaderFlyweight.hasMetadata(request)); assertEquals("d", data); - assertTrue(metadata.readableBytes() == 0); + assertNull(metadata); request.release(); } diff --git a/rsocket-core/src/test/java/io/rsocket/util/ByteBufPayloadTest.java b/rsocket-core/src/test/java/io/rsocket/util/ByteBufPayloadTest.java new file mode 100644 index 000000000..2ad944d09 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/util/ByteBufPayloadTest.java @@ -0,0 +1,64 @@ +package io.rsocket.util; + +import io.netty.buffer.Unpooled; +import io.netty.util.IllegalReferenceCountException; +import io.rsocket.Payload; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ByteBufPayloadTest { + + @Test + public void shouldIndicateThatItHasMetadata() { + Payload payload = ByteBufPayload.create("data", "metadata"); + + Assertions.assertThat(payload.hasMetadata()).isTrue(); + Assertions.assertThat(payload.release()).isTrue(); + } + + @Test + public void shouldIndicateThatItHasNotMetadata() { + Payload payload = ByteBufPayload.create("data"); + + Assertions.assertThat(payload.hasMetadata()).isFalse(); + Assertions.assertThat(payload.release()).isTrue(); + } + + @Test + public void shouldIndicateThatItHasMetadata1() { + Payload payload = + ByteBufPayload.create(Unpooled.wrappedBuffer("data".getBytes()), Unpooled.EMPTY_BUFFER); + + Assertions.assertThat(payload.hasMetadata()).isTrue(); + Assertions.assertThat(payload.release()).isTrue(); + } + + @Test + public void shouldThrowExceptionIfAccessAfterRelease() { + Payload payload = ByteBufPayload.create("data", "metadata"); + + Assertions.assertThat(payload.release()).isTrue(); + + Assertions.assertThatThrownBy(payload::hasMetadata) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::data).isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::metadata) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::sliceData) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::sliceMetadata) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::touch) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(() -> payload.touch("test")) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::getData) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::getMetadata) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::getDataUtf8) + .isInstanceOf(IllegalReferenceCountException.class); + Assertions.assertThatThrownBy(payload::getMetadataUtf8) + .isInstanceOf(IllegalReferenceCountException.class); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/util/DefaultPayloadTest.java b/rsocket-core/src/test/java/io/rsocket/util/DefaultPayloadTest.java index 45ee4eacb..6bae0886b 100644 --- a/rsocket-core/src/test/java/io/rsocket/util/DefaultPayloadTest.java +++ b/rsocket-core/src/test/java/io/rsocket/util/DefaultPayloadTest.java @@ -16,10 +16,13 @@ package io.rsocket.util; -import static org.hamcrest.MatcherAssert.*; -import static org.hamcrest.Matchers.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import io.netty.buffer.Unpooled; import io.rsocket.Payload; +import java.nio.ByteBuffer; +import org.assertj.core.api.Assertions; import org.junit.Test; public class DefaultPayloadTest { @@ -48,4 +51,27 @@ public void staticMethods() { assertDataAndMetadata(DefaultPayload.create(DATA_VAL, METADATA_VAL), DATA_VAL, METADATA_VAL); assertDataAndMetadata(DefaultPayload.create(DATA_VAL), DATA_VAL, null); } + + @Test + public void shouldIndicateThatItHasNotMetadata() { + Payload payload = DefaultPayload.create("data"); + + Assertions.assertThat(payload.hasMetadata()).isFalse(); + } + + @Test + public void shouldIndicateThatItHasMetadata1() { + Payload payload = + DefaultPayload.create(Unpooled.wrappedBuffer("data".getBytes()), Unpooled.EMPTY_BUFFER); + + Assertions.assertThat(payload.hasMetadata()).isTrue(); + } + + @Test + public void shouldIndicateThatItHasMetadata2() { + Payload payload = + DefaultPayload.create(ByteBuffer.wrap("data".getBytes()), ByteBuffer.allocate(0)); + + Assertions.assertThat(payload.hasMetadata()).isTrue(); + } } diff --git a/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java index 2651b14ec..60ff05124 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java @@ -87,12 +87,12 @@ public static ByteBuf createTestRequestNFrame() { /** @return {@link ByteBuf} representing test instance of Request-Response frame */ public static ByteBuf createTestRequestResponseFrame() { - return RequestResponseFrameFlyweight.encode(allocator, 1, false, emptyPayload); + return RequestResponseFrameFlyweight.encodeReleasingPayload(allocator, 1, emptyPayload); } /** @return {@link ByteBuf} representing test instance of Request-Stream frame */ public static ByteBuf createTestRequestStreamFrame() { - return RequestStreamFrameFlyweight.encode(allocator, 1, false, 1L, emptyPayload); + return RequestStreamFrameFlyweight.encodeReleasingPayload(allocator, 1, 1L, emptyPayload); } /** @return {@link ByteBuf} representing test instance of Setup frame */ diff --git a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java index e4ff75b2a..583f58634 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -35,10 +35,12 @@ import java.util.zip.GZIPInputStream; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; 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.test.StepVerifier; @@ -64,9 +66,15 @@ static String read(String resourceName) { } } + @BeforeEach + default void setUp() { + Hooks.onOperatorDebug(); + } + @AfterEach default void close() { getTransportPair().dispose(); + Hooks.resetOnOperatorDebug(); } default Payload createTestPayload(int metadataPresent) { @@ -175,7 +183,7 @@ default void requestChannel200_000() { .verify(getTimeout()); } - @DisplayName("makes 1 requestChannel request with 2,000 large payloads") + @DisplayName("makes 1 requestChannel request with 200 large payloads") @Test default void largePayloadRequestChannel200() { Flux payloads = Flux.range(0, 200).map(__ -> LARGE_PAYLOAD); From b3087efc5f3d44f02542c5e560c2534f092b376a Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 24 Apr 2020 19:50:54 +0300 Subject: [PATCH 146/181] Refactors how `ByteBufAllocator` is used and setup (#796) --- .../java/io/rsocket/DuplexConnection.java | 8 ++++ .../main/java/io/rsocket/RSocketFactory.java | 47 ++++++++++++++++++- .../io/rsocket/core/RSocketConnector.java | 19 +------- .../io/rsocket/core/RSocketRequester.java | 5 +- .../io/rsocket/core/RSocketResponder.java | 3 +- .../java/io/rsocket/core/RSocketServer.java | 23 ++------- .../java/io/rsocket/core/ServerSetup.java | 23 ++------- .../FragmentationDuplexConnection.java | 20 +++----- .../ReassemblyDuplexConnection.java | 11 +++-- .../ClientServerInputMultiplexer.java | 6 +++ .../rsocket/resume/ClientRSocketSession.java | 3 +- .../resume/ResumableDuplexConnection.java | 6 +++ .../rsocket/resume/ServerRSocketSession.java | 3 +- .../rsocket/util/DuplexConnectionProxy.java | 6 +++ .../io/rsocket/core/AbstractSocketRule.java | 8 ++-- .../java/io/rsocket/core/KeepAliveTest.java | 38 +++++++++++---- .../io/rsocket/core/RSocketLeaseTest.java | 8 ++-- .../core/RSocketRequesterSubscribersTest.java | 6 ++- .../io/rsocket/core/RSocketRequesterTest.java | 4 +- .../io/rsocket/core/RSocketResponderTest.java | 8 ++-- .../java/io/rsocket/core/RSocketTest.java | 14 ++++-- .../io/rsocket/core/SetupRejectionTest.java | 18 ++++--- .../FragmentationDuplexConnectionTest.java | 24 ++++------ .../ReassembleDuplexConnectionTest.java | 19 +++++--- .../ClientServerInputMultiplexerTest.java | 6 ++- .../test/util/LocalDuplexConnection.java | 15 +++++- .../test/util/TestClientTransport.java | 11 ++++- .../test/util/TestDuplexConnection.java | 24 ++++++---- .../test/util/TestServerTransport.java | 10 +++- .../transport/ws/WebSocketHeadersSample.java | 5 +- .../MicrometerDuplexConnection.java | 6 +++ .../transport/local/LocalClientTransport.java | 35 ++++++++++---- .../local/LocalDuplexConnection.java | 14 +++++- .../transport/local/LocalServerTransport.java | 7 +-- .../transport/netty/TcpDuplexConnection.java | 5 ++ .../netty/WebsocketDuplexConnection.java | 6 +++ .../netty/client/TcpClientTransport.java | 10 +--- .../client/WebsocketClientTransport.java | 7 +-- .../netty/server/TcpServerTransport.java | 11 +---- .../netty/server/WebsocketRouteTransport.java | 7 +-- .../server/WebsocketServerTransport.java | 8 +--- 41 files changed, 305 insertions(+), 212 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java index b87ed0570..6190d24e3 100644 --- a/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/DuplexConnection.java @@ -17,6 +17,7 @@ package io.rsocket; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import java.nio.channels.ClosedChannelException; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -78,6 +79,13 @@ default Mono sendOne(ByteBuf frame) { */ Flux receive(); + /** + * Returns the assigned {@link ByteBufAllocator}. + * + * @return the {@link ByteBufAllocator} + */ + ByteBufAllocator alloc(); + @Override default double availability() { return isDisposed() ? 0.0 : 1.0; diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 0e1ae12cb..43d344b9d 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -113,8 +113,29 @@ public ClientRSocketFactory(RSocketConnector connector) { this.connector = connector; } + /** + * @deprecated this method is deprecated and deliberately has no effect anymore. Right now, in + * order configure the custom {@link ByteBufAllocator} it is recommended to use the + * following setup for Reactor Netty based transport:
    + * 1. For Client:
    + *

    {@code
    +     * TcpClient.create()
    +     *          ...
    +     *          .bootstrap(bootstrap -> bootstrap.option(ChannelOption.ALLOCATOR, clientAllocator))
    +     * }
    + *
    + * 2. For server:
    + *
    {@code
    +     * TcpServer.create()
    +     *          ...
    +     *          .bootstrap(serverBootstrap -> serverBootstrap.childOption(ChannelOption.ALLOCATOR, serverAllocator))
    +     * }
    + * Or in case of local transport, to use corresponding factory method {@code + * LocalClientTransport.creat(String, ByteBufAllocator)} + * @param allocator instance of {@link ByteBufAllocator} + * @return this factory instance + */ public ClientRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { - connector.byteBufAllocator(allocator); return this; } @@ -395,8 +416,30 @@ public ServerRSocketFactory(RSocketServer server) { this.server = server; } + /** + * @deprecated this method is deprecated and deliberately has no effect anymore. Right now, in + * order configure the custom {@link ByteBufAllocator} it is recommended to use the + * following setup for Reactor Netty based transport:
    + * 1. For Client:
    + *
    {@code
    +     * TcpClient.create()
    +     *          ...
    +     *          .bootstrap(bootstrap -> bootstrap.option(ChannelOption.ALLOCATOR, clientAllocator))
    +     * }
    + *
    + * 2. For server:
    + *
    {@code
    +     * TcpServer.create()
    +     *          ...
    +     *          .bootstrap(serverBootstrap -> serverBootstrap.childOption(ChannelOption.ALLOCATOR, serverAllocator))
    +     * }
    + * Or in case of local transport, to use corresponding factory method {@code + * LocalClientTransport.creat(String, ByteBufAllocator)} + * @param allocator instance of {@link ByteBufAllocator} + * @return this factory instance + */ + @Deprecated public ServerRSocketFactory byteBufAllocator(ByteBufAllocator allocator) { - server.byteBufAllocator(allocator); return this; } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 23ecb9ba6..dc5be2430 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -16,7 +16,6 @@ package io.rsocket.core; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.rsocket.AbstractRSocket; import io.rsocket.ConnectionSetupPayload; @@ -71,7 +70,6 @@ public class RSocketConnector { private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; private Consumer errorConsumer = Throwable::printStackTrace; - private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; private RSocketConnector() {} @@ -241,16 +239,6 @@ public RSocketConnector errorConsumer(Consumer errorConsumer) { return this; } - /** - * @deprecated this is deprecated with no replacement and will be removed after {@link - * io.rsocket.RSocketFactory} is removed. - */ - public RSocketConnector byteBufAllocator(ByteBufAllocator allocator) { - Objects.requireNonNull(allocator); - this.allocator = allocator; - return this; - } - public Mono connect(ClientTransport transport) { return connect(() -> transport); } @@ -289,7 +277,6 @@ public Mono connect(Supplier transportSupplier) { RSocket rSocketRequester = new RSocketRequester( - allocator, multiplexer.asClientConnection(), payloadDecoder, errorConsumer, @@ -304,7 +291,7 @@ public Mono connect(Supplier transportSupplier) { ByteBuf setupFrame = SetupFrameFlyweight.encode( - allocator, + wrappedConnection.alloc(), leaseEnabled, (int) keepAliveInterval.toMillis(), (int) keepAliveMaxLifeTime.toMillis(), @@ -326,7 +313,7 @@ public Mono connect(Supplier transportSupplier) { leaseEnabled ? new ResponderLeaseHandler.Impl<>( CLIENT_TAG, - allocator, + wrappedConnection.alloc(), leases.sender(), errorConsumer, leases.stats()) @@ -334,7 +321,6 @@ public Mono connect(Supplier transportSupplier) { RSocket rSocketResponder = new RSocketResponder( - allocator, multiplexer.asServerConnection(), wrappedRSocketHandler, payloadDecoder, @@ -364,7 +350,6 @@ private ClientRSocketSession createSession( ClientRSocketSession session = new ClientRSocketSession( connection, - allocator, resume.getSessionDuration(), resume.getResumeStrategySupplier(), resume.getStoreFactory(CLIENT_TAG).apply(resumeToken), diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 42a6a524d..04c766eec 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -108,7 +108,6 @@ class RSocketRequester implements RSocket { private volatile Throwable terminationError; RSocketRequester( - ByteBufAllocator allocator, DuplexConnection connection, PayloadDecoder payloadDecoder, Consumer errorConsumer, @@ -118,8 +117,8 @@ class RSocketRequester implements RSocket { int keepAliveAckTimeout, @Nullable KeepAliveHandler keepAliveHandler, RequesterLeaseHandler leaseHandler) { - this.allocator = allocator; this.connection = connection; + this.allocator = connection.alloc(); this.payloadDecoder = payloadDecoder; this.errorConsumer = errorConsumer; this.streamIdSupplier = streamIdSupplier; @@ -141,7 +140,7 @@ class RSocketRequester implements RSocket { if (keepAliveTickPeriod != 0 && keepAliveHandler != null) { KeepAliveSupport keepAliveSupport = - new ClientKeepAliveSupport(allocator, keepAliveTickPeriod, keepAliveAckTimeout); + new ClientKeepAliveSupport(this.allocator, keepAliveTickPeriod, keepAliveAckTimeout); this.keepAliveFramesAcceptor = keepAliveHandler.start( keepAliveSupport, sendProcessor::onNextPrioritized, this::tryTerminateOnKeepAlive); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index 5aef7eed2..f992b7577 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -75,15 +75,14 @@ class RSocketResponder implements ResponderRSocket { private final ByteBufAllocator allocator; RSocketResponder( - ByteBufAllocator allocator, DuplexConnection connection, RSocket requestHandler, PayloadDecoder payloadDecoder, Consumer errorConsumer, ResponderLeaseHandler leaseHandler, int mtu) { - this.allocator = allocator; this.connection = connection; + this.allocator = connection.alloc(); this.mtu = mtu; this.requestHandler = requestHandler; diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index 113b4283f..6c289d707 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -17,7 +17,6 @@ package io.rsocket.core; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; import io.rsocket.AbstractRSocket; import io.rsocket.Closeable; import io.rsocket.ConnectionSetupPayload; @@ -55,7 +54,6 @@ public final class RSocketServer { private Consumer errorConsumer = Throwable::printStackTrace; private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; - private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; private RSocketServer() {} @@ -114,17 +112,6 @@ public RSocketServer errorConsumer(Consumer errorConsumer) { return this; } - /** - * @deprecated this is deprecated with no replacement and will be removed after {@link - * io.rsocket.RSocketFactory} is removed. - */ - @Deprecated - public RSocketServer byteBufAllocator(ByteBufAllocator allocator) { - Objects.requireNonNull(allocator); - this.allocator = allocator; - return this; - } - public ServerTransport.ConnectionAcceptor asConnectionAcceptor() { return new ServerTransport.ConnectionAcceptor() { private final ServerSetup serverSetup = serverSetup(); @@ -228,7 +215,6 @@ private Mono acceptSetup( RSocket rSocketRequester = new RSocketRequester( - allocator, wrappedMultiplexer.asServerConnection(), payloadDecoder, errorConsumer, @@ -252,12 +238,13 @@ private Mono acceptSetup( .doOnNext( rSocketHandler -> { RSocket wrappedRSocketHandler = interceptors.initResponder(rSocketHandler); + DuplexConnection connection = wrappedMultiplexer.asClientConnection(); ResponderLeaseHandler responderLeaseHandler = leaseEnabled ? new ResponderLeaseHandler.Impl<>( SERVER_TAG, - allocator, + connection.alloc(), leases.sender(), errorConsumer, leases.stats()) @@ -265,8 +252,7 @@ private Mono acceptSetup( RSocket rSocketResponder = new RSocketResponder( - allocator, - wrappedMultiplexer.asClientConnection(), + connection, wrappedRSocketHandler, payloadDecoder, errorConsumer, @@ -279,12 +265,11 @@ private Mono acceptSetup( } private ServerSetup serverSetup() { - return resume != null ? createSetup() : new ServerSetup.DefaultServerSetup(allocator); + return resume != null ? createSetup() : new ServerSetup.DefaultServerSetup(); } ServerSetup createSetup() { return new ServerSetup.ResumableServerSetup( - allocator, new SessionManager(), resume.getSessionDuration(), resume.getStreamTimeout(), diff --git a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java index 16f5f61b1..3e20d3c60 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -19,7 +19,7 @@ import static io.rsocket.keepalive.KeepAliveHandler.*; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; +import io.rsocket.DuplexConnection; import io.rsocket.exceptions.RejectedResumeException; import io.rsocket.exceptions.UnsupportedSetupException; import io.rsocket.frame.ErrorFrameFlyweight; @@ -35,12 +35,6 @@ abstract class ServerSetup { - final ByteBufAllocator allocator; - - public ServerSetup(ByteBufAllocator allocator) { - this.allocator = allocator; - } - abstract Mono acceptRSocketSetup( ByteBuf frame, ClientServerInputMultiplexer multiplexer, @@ -51,18 +45,14 @@ abstract Mono acceptRSocketSetup( void dispose() {} Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { - return multiplexer - .asSetupConnection() - .sendOne(ErrorFrameFlyweight.encode(allocator, 0, exception)) + DuplexConnection duplexConnection = multiplexer.asSetupConnection(); + return duplexConnection + .sendOne(ErrorFrameFlyweight.encode(duplexConnection.alloc(), 0, exception)) .onErrorResume(err -> Mono.empty()); } static class DefaultServerSetup extends ServerSetup { - DefaultServerSetup(ByteBufAllocator allocator) { - super(allocator); - } - @Override public Mono acceptRSocketSetup( ByteBuf frame, @@ -94,7 +84,6 @@ public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexe } static class ResumableServerSetup extends ServerSetup { - private final ByteBufAllocator allocator; private final SessionManager sessionManager; private final Duration resumeSessionDuration; private final Duration resumeStreamTimeout; @@ -102,14 +91,11 @@ static class ResumableServerSetup extends ServerSetup { private final boolean cleanupStoreOnKeepAlive; ResumableServerSetup( - ByteBufAllocator allocator, SessionManager sessionManager, Duration resumeSessionDuration, Duration resumeStreamTimeout, Function resumeStoreFactory, boolean cleanupStoreOnKeepAlive) { - super(allocator); - this.allocator = allocator; this.sessionManager = sessionManager; this.resumeSessionDuration = resumeSessionDuration; this.resumeStreamTimeout = resumeStreamTimeout; @@ -131,7 +117,6 @@ public Mono acceptRSocketSetup( .save( new ServerRSocketSession( multiplexer.asClientServerConnection(), - allocator, resumeSessionDuration, resumeStreamTimeout, resumeStoreFactory, diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java index 333787e9f..316643e10 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java @@ -19,7 +19,6 @@ import static io.rsocket.fragmentation.FrameFragmenter.fragmentFrame; import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.ByteBufUtil; import io.rsocket.DuplexConnection; import io.rsocket.frame.FrameHeaderFlyweight; @@ -46,26 +45,19 @@ public final class FragmentationDuplexConnection extends ReassemblyDuplexConnect private static final Logger logger = LoggerFactory.getLogger(FragmentationDuplexConnection.class); private final DuplexConnection delegate; private final int mtu; - private final ByteBufAllocator allocator; private final FrameReassembler frameReassembler; private final boolean encodeLength; private final String type; public FragmentationDuplexConnection( - DuplexConnection delegate, - ByteBufAllocator allocator, - int mtu, - boolean encodeAndEncodeLength, - String type) { - super(delegate, allocator, encodeAndEncodeLength); + DuplexConnection delegate, int mtu, boolean encodeAndEncodeLength, String type) { + super(delegate, encodeAndEncodeLength); Objects.requireNonNull(delegate, "delegate must not be null"); - Objects.requireNonNull(allocator, "byteBufAllocator must not be null"); this.encodeLength = encodeAndEncodeLength; - this.allocator = allocator; this.delegate = delegate; this.mtu = assertMtu(mtu); - this.frameReassembler = new FrameReassembler(allocator); + this.frameReassembler = new FrameReassembler(delegate.alloc()); this.type = type; delegate.onClose().doFinally(s -> frameReassembler.dispose()).subscribe(); @@ -113,7 +105,7 @@ public Mono sendOne(ByteBuf frame) { if (shouldFragment(frameType, readableBytes)) { if (logger.isDebugEnabled()) { return delegate.send( - Flux.from(fragmentFrame(allocator, mtu, frame, frameType, encodeLength)) + Flux.from(fragmentFrame(alloc(), mtu, frame, frameType, encodeLength)) .doOnNext( byteBuf -> { ByteBuf f = encodeLength ? FrameLengthFlyweight.frame(byteBuf) : byteBuf; @@ -126,7 +118,7 @@ public Mono sendOne(ByteBuf frame) { })); } else { return delegate.send( - Flux.from(fragmentFrame(allocator, mtu, frame, frameType, encodeLength))); + Flux.from(fragmentFrame(alloc(), mtu, frame, frameType, encodeLength))); } } else { return delegate.sendOne(encode(frame)); @@ -135,7 +127,7 @@ public Mono sendOne(ByteBuf frame) { private ByteBuf encode(ByteBuf frame) { if (encodeLength) { - return FrameLengthFlyweight.encode(allocator, frame.readableBytes(), frame); + return FrameLengthFlyweight.encode(alloc(), frame.readableBytes(), frame); } else { return frame; } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java index bf0d7482c..933755bb2 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java @@ -37,13 +37,11 @@ public class ReassemblyDuplexConnection implements DuplexConnection { private final FrameReassembler frameReassembler; private final boolean decodeLength; - public ReassemblyDuplexConnection( - DuplexConnection delegate, ByteBufAllocator allocator, boolean decodeLength) { + public ReassemblyDuplexConnection(DuplexConnection delegate, boolean decodeLength) { Objects.requireNonNull(delegate, "delegate must not be null"); - Objects.requireNonNull(allocator, "byteBufAllocator must not be null"); this.decodeLength = decodeLength; this.delegate = delegate; - this.frameReassembler = new FrameReassembler(allocator); + this.frameReassembler = new FrameReassembler(delegate.alloc()); delegate.onClose().doFinally(s -> frameReassembler.dispose()).subscribe(); } @@ -77,6 +75,11 @@ public Flux receive() { }); } + @Override + public ByteBufAllocator alloc() { + return delegate.alloc(); + } + @Override public Mono onClose() { return delegate.onClose(); diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index 68098e279..cf3eeb120 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -17,6 +17,7 @@ package io.rsocket.internal; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.Closeable; import io.rsocket.DuplexConnection; import io.rsocket.frame.FrameHeaderFlyweight; @@ -201,6 +202,11 @@ public Flux receive() { })); } + @Override + public ByteBufAllocator alloc() { + return source.alloc(); + } + @Override public void dispose() { source.dispose(); diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java index b347642e3..509fb5168 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -41,13 +41,12 @@ public class ClientRSocketSession implements RSocketSession resumeStrategy, ResumableFramesStore resumableFramesStore, Duration resumeStreamTimeout, boolean cleanupStoreOnKeepAlive) { - this.allocator = allocator; + this.allocator = duplexConnection.alloc(); this.resumableConnection = new ResumableDuplexConnection( "client", 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 49401d560..b9c93f4cd 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -17,6 +17,7 @@ package io.rsocket.resume; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.Closeable; import io.rsocket.DuplexConnection; import io.rsocket.frame.FrameHeaderFlyweight; @@ -105,6 +106,11 @@ public ResumableDuplexConnection( reconnect(duplexConnection); } + @Override + public ByteBufAllocator alloc() { + return curConnection.alloc(); + } + public void disconnect() { DuplexConnection c = this.curConnection; if (c != null) { diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java index 1a0605497..5d55559cc 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -43,13 +43,12 @@ public class ServerRSocketSession implements RSocketSession { public ServerRSocketSession( DuplexConnection duplexConnection, - ByteBufAllocator allocator, Duration resumeSessionDuration, Duration resumeStreamTimeout, Function resumeStoreFactory, ByteBuf resumeToken, boolean cleanupStoreOnKeepAlive) { - this.allocator = allocator; + this.allocator = duplexConnection.alloc(); this.resumeToken = resumeToken; this.resumableConnection = new ResumableDuplexConnection( diff --git a/rsocket-core/src/main/java/io/rsocket/util/DuplexConnectionProxy.java b/rsocket-core/src/main/java/io/rsocket/util/DuplexConnectionProxy.java index fa19553a7..2f5d1da4b 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/DuplexConnectionProxy.java +++ b/rsocket-core/src/main/java/io/rsocket/util/DuplexConnectionProxy.java @@ -17,6 +17,7 @@ package io.rsocket.util; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -44,6 +45,11 @@ public double availability() { return connection.availability(); } + @Override + public ByteBufAllocator alloc() { + return connection.alloc(); + } + @Override public Mono onClose() { return connection.onClose(); diff --git a/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java index 5a43838c7..20972a0d3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java +++ b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java @@ -41,10 +41,10 @@ public Statement apply(final Statement base, Description description) { return new Statement() { @Override public void evaluate() throws Throwable { - connection = new TestDuplexConnection(); + allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + connection = new TestDuplexConnection(allocator); connectSub = TestSubscriber.create(); errors = new ConcurrentLinkedQueue<>(); - allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); init(); base.evaluate(); } @@ -52,10 +52,10 @@ public void evaluate() throws Throwable { } protected void init() { - socket = newRSocket(allocator); + socket = newRSocket(); } - protected abstract T newRSocket(LeaksTrackingByteBufAllocator allocator); + protected abstract T newRSocket(); public void assertNoConnectionErrors() { if (errors.size() > 1) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java index 10725238a..e8f3f4190 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java @@ -23,6 +23,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.rsocket.RSocket; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; @@ -52,11 +53,12 @@ public class KeepAliveTest { private ResumableRSocketState resumableRequesterState; static RSocketState requester(int tickPeriod, int timeout) { - TestDuplexConnection connection = new TestDuplexConnection(); + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + TestDuplexConnection connection = new TestDuplexConnection(allocator); Errors errors = new Errors(); RSocketRequester rSocket = new RSocketRequester( - ByteBufAllocator.DEFAULT, connection, DefaultPayload::create, errors, @@ -66,11 +68,13 @@ static RSocketState requester(int tickPeriod, int timeout) { timeout, new DefaultKeepAliveHandler(connection), RequesterLeaseHandler.None); - return new RSocketState(rSocket, errors, connection); + return new RSocketState(rSocket, errors, allocator, connection); } static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { - TestDuplexConnection connection = new TestDuplexConnection(); + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + TestDuplexConnection connection = new TestDuplexConnection(allocator); ResumableDuplexConnection resumableConnection = new ResumableDuplexConnection( "test", @@ -82,7 +86,6 @@ static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { Errors errors = new Errors(); RSocketRequester rSocket = new RSocketRequester( - ByteBufAllocator.DEFAULT, resumableConnection, DefaultPayload::create, errors, @@ -92,7 +95,7 @@ static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { timeout, new ResumableKeepAliveHandler(resumableConnection), RequesterLeaseHandler.None); - return new ResumableRSocketState(rSocket, errors, connection, resumableConnection); + return new ResumableRSocketState(rSocket, errors, connection, resumableConnection, allocator); } @BeforeEach @@ -194,7 +197,7 @@ void resumableRequesterKeepAlivesAfterReconnect() { resumableRequester(KEEP_ALIVE_INTERVAL, KEEP_ALIVE_TIMEOUT); ResumableDuplexConnection resumableDuplexConnection = rSocketState.resumableDuplexConnection(); resumableDuplexConnection.disconnect(); - TestDuplexConnection newTestConnection = new TestDuplexConnection(); + TestDuplexConnection newTestConnection = new TestDuplexConnection(rSocketState.alloc()); resumableDuplexConnection.reconnect(newTestConnection); resumableDuplexConnection.resume(0, 0, ignored -> Mono.empty()); @@ -244,11 +247,17 @@ static class RSocketState { private final RSocket rSocket; private final Errors errors; private final TestDuplexConnection connection; + private final LeaksTrackingByteBufAllocator allocator; - public RSocketState(RSocket rSocket, Errors errors, TestDuplexConnection connection) { + public RSocketState( + RSocket rSocket, + Errors errors, + LeaksTrackingByteBufAllocator allocator, + TestDuplexConnection connection) { this.rSocket = rSocket; this.errors = errors; this.connection = connection; + this.allocator = allocator; } public TestDuplexConnection connection() { @@ -262,6 +271,10 @@ public RSocket rSocket() { public Errors errors() { return errors; } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } } static class ResumableRSocketState { @@ -269,16 +282,19 @@ static class ResumableRSocketState { private final Errors errors; private final TestDuplexConnection connection; private final ResumableDuplexConnection resumableDuplexConnection; + private final LeaksTrackingByteBufAllocator allocator; public ResumableRSocketState( RSocket rSocket, Errors errors, TestDuplexConnection connection, - ResumableDuplexConnection resumableDuplexConnection) { + ResumableDuplexConnection resumableDuplexConnection, + LeaksTrackingByteBufAllocator allocator) { this.rSocket = rSocket; this.errors = errors; this.connection = connection; this.resumableDuplexConnection = resumableDuplexConnection; + this.allocator = allocator; } public TestDuplexConnection connection() { @@ -296,6 +312,10 @@ public RSocket rSocket() { public Errors errors() { return errors; } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } } static class Errors implements Consumer { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java index 01ee1eb6d..51f5afc24 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -26,9 +26,9 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; -import io.netty.buffer.UnpooledByteBufAllocator; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.Exceptions; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; @@ -77,10 +77,10 @@ class RSocketLeaseTest { @BeforeEach void setUp() { - connection = new TestDuplexConnection(); PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; - byteBufAllocator = UnpooledByteBufAllocator.DEFAULT; + byteBufAllocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + connection = new TestDuplexConnection(byteBufAllocator); requesterLeaseHandler = new RequesterLeaseHandler.Impl(TAG, leases -> leaseReceiver = leases); responderLeaseHandler = new ResponderLeaseHandler.Impl<>( @@ -90,7 +90,6 @@ void setUp() { new ClientServerInputMultiplexer(connection, new InitializingInterceptorRegistry(), true); rSocketRequester = new RSocketRequester( - byteBufAllocator, multiplexer.asClientConnection(), payloadDecoder, err -> {}, @@ -110,7 +109,6 @@ void setUp() { rSocketResponder = new RSocketResponder( - byteBufAllocator, multiplexer.asServerConnection(), mockRSocketHandler, payloadDecoder, diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index 8380290f2..01cf99e26 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.RSocket; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; import io.rsocket.frame.decoder.PayloadDecoder; @@ -52,15 +53,16 @@ class RSocketRequesterSubscribersTest { FrameType.REQUEST_STREAM, FrameType.REQUEST_CHANNEL)); + private LeaksTrackingByteBufAllocator allocator; private RSocket rSocketRequester; private TestDuplexConnection connection; @BeforeEach void setUp() { - connection = new TestDuplexConnection(); + allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + connection = new TestDuplexConnection(allocator); rSocketRequester = new RSocketRequester( - ByteBufAllocator.DEFAULT, connection, PayloadDecoder.DEFAULT, err -> {}, diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index bc19d8132..e536d2db4 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -41,7 +41,6 @@ import io.netty.util.ReferenceCounted; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.CustomRSocketException; import io.rsocket.exceptions.RejectedSetupException; @@ -741,9 +740,8 @@ public int sendRequestResponse(Publisher response) { public static class ClientSocketRule extends AbstractSocketRule { @Override - protected RSocketRequester newRSocket(LeaksTrackingByteBufAllocator allocator) { + protected RSocketRequester newRSocket() { return new RSocketRequester( - allocator, connection, PayloadDecoder.ZERO_COPY, throwable -> errors.add(throwable), diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index 78027aa3d..48910b3a2 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -38,7 +38,6 @@ import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.CancelFrameFlyweight; import io.rsocket.frame.ErrorFrameFlyweight; import io.rsocket.frame.FrameHeaderFlyweight; @@ -735,7 +734,7 @@ public Mono requestResponse(Payload payload) { public void setAcceptingSocket(RSocket acceptingSocket) { this.acceptingSocket = acceptingSocket; - connection = new TestDuplexConnection(); + connection = new TestDuplexConnection(alloc()); connectSub = TestSubscriber.create(); errors = new ConcurrentLinkedQueue<>(); this.prefetch = Integer.MAX_VALUE; @@ -744,7 +743,7 @@ public void setAcceptingSocket(RSocket acceptingSocket) { public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { this.acceptingSocket = acceptingSocket; - connection = new TestDuplexConnection(); + connection = new TestDuplexConnection(alloc()); connectSub = TestSubscriber.create(); errors = new ConcurrentLinkedQueue<>(); this.prefetch = prefetch; @@ -752,9 +751,8 @@ public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { } @Override - protected RSocketResponder newRSocket(LeaksTrackingByteBufAllocator allocator) { + protected RSocketResponder newRSocket() { return new RSocketResponder( - allocator, connection, acceptingSocket, PayloadDecoder.ZERO_COPY, diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index 568f8eed6..4a2c43ef8 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -26,6 +26,7 @@ import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.CustomRSocketException; import io.rsocket.frame.decoder.PayloadDecoder; @@ -415,6 +416,8 @@ public static class SocketRule extends ExternalResource { private ArrayList clientErrors = new ArrayList<>(); private ArrayList serverErrors = new ArrayList<>(); + private LeaksTrackingByteBufAllocator allocator; + @Override public Statement apply(Statement base, Description description) { return new Statement() { @@ -426,14 +429,19 @@ public void evaluate() throws Throwable { }; } + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } + protected void init() { + allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); serverProcessor = DirectProcessor.create(); clientProcessor = DirectProcessor.create(); LocalDuplexConnection serverConnection = - new LocalDuplexConnection("server", clientProcessor, serverProcessor); + new LocalDuplexConnection("server", allocator, clientProcessor, serverProcessor); LocalDuplexConnection clientConnection = - new LocalDuplexConnection("client", serverProcessor, clientProcessor); + new LocalDuplexConnection("client", allocator, serverProcessor, clientProcessor); requestAcceptor = null != requestAcceptor @@ -468,7 +476,6 @@ public Flux requestChannel(Publisher payloads) { srs = new RSocketResponder( - ByteBufAllocator.DEFAULT, serverConnection, requestAcceptor, PayloadDecoder.DEFAULT, @@ -478,7 +485,6 @@ public Flux requestChannel(Publisher payloads) { crs = new RSocketRequester( - ByteBufAllocator.DEFAULT, clientConnection, PayloadDecoder.DEFAULT, throwable -> clientErrors.add(throwable), diff --git a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java index 5b489896b..db72c7775 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -6,6 +6,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.*; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.Exceptions; import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.frame.ErrorFrameFlyweight; @@ -47,11 +48,12 @@ void responderRejectSetup() { @Test void requesterStreamsTerminatedOnZeroErrorFrame() { - TestDuplexConnection conn = new TestDuplexConnection(); + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + TestDuplexConnection conn = new TestDuplexConnection(allocator); List errors = new ArrayList<>(); RSocketRequester rSocket = new RSocketRequester( - ByteBufAllocator.DEFAULT, conn, DefaultPayload::create, errors::add, @@ -84,10 +86,11 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { @Test void requesterNewStreamsTerminatedAfterZeroErrorFrame() { - TestDuplexConnection conn = new TestDuplexConnection(); + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + TestDuplexConnection conn = new TestDuplexConnection(allocator); RSocketRequester rSocket = new RSocketRequester( - ByteBufAllocator.DEFAULT, conn, DefaultPayload::create, err -> {}, @@ -132,7 +135,9 @@ public Mono senderRSocket() { private static class SingleConnectionTransport implements ServerTransport { - private final TestDuplexConnection conn = new TestDuplexConnection(); + private final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + private final TestDuplexConnection conn = new TestDuplexConnection(allocator); @Override public Mono start(ConnectionAcceptor acceptor, int mtu) { @@ -150,8 +155,7 @@ public ByteBuf awaitSent() { public void connect() { Payload payload = DefaultPayload.create(DefaultPayload.EMPTY_BUFFER); ByteBuf setup = - SetupFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, false, 0, 42, "mdMime", "dMime", payload); + SetupFrameFlyweight.encode(allocator, false, 0, 42, "mdMime", "dMime", payload); conn.addToReceivedBuffer(setup); } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java index a6918c497..9050eaa90 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java @@ -24,6 +24,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.rsocket.DuplexConnection; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.*; import java.util.concurrent.ThreadLocalRandom; import org.junit.Assert; @@ -55,16 +56,14 @@ final class FragmentationDuplexConnectionTest { private final ArgumentCaptor> publishers = ArgumentCaptor.forClass(Publisher.class); - private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + private LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); @DisplayName("constructor throws IllegalArgumentException with negative maxFragmentLength") @Test void constructorInvalidMaxFragmentSize() { assertThatIllegalArgumentException() - .isThrownBy( - () -> - new FragmentationDuplexConnection( - delegate, allocator, Integer.MIN_VALUE, false, "")) + .isThrownBy(() -> new FragmentationDuplexConnection(delegate, Integer.MIN_VALUE, false, "")) .withMessage("smallest allowed mtu size is 64 bytes, provided: -2147483648"); } @@ -72,23 +71,15 @@ void constructorInvalidMaxFragmentSize() { @Test void constructorMtuLessThanMin() { assertThatIllegalArgumentException() - .isThrownBy(() -> new FragmentationDuplexConnection(delegate, allocator, 2, false, "")) + .isThrownBy(() -> new FragmentationDuplexConnection(delegate, 2, false, "")) .withMessage("smallest allowed mtu size is 64 bytes, provided: 2"); } - @DisplayName("constructor throws NullPointerException with null byteBufAllocator") - @Test - void constructorNullByteBufAllocator() { - assertThatNullPointerException() - .isThrownBy(() -> new FragmentationDuplexConnection(delegate, null, 64, false, "")) - .withMessage("byteBufAllocator must not be null"); - } - @DisplayName("constructor throws NullPointerException with null delegate") @Test void constructorNullDelegate() { assertThatNullPointerException() - .isThrownBy(() -> new FragmentationDuplexConnection(null, allocator, 64, false, "")) + .isThrownBy(() -> new FragmentationDuplexConnection(null, 64, false, "")) .withMessage("delegate must not be null"); } @@ -100,8 +91,9 @@ void sendData() { allocator, 1, false, Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(data)); when(delegate.onClose()).thenReturn(Mono.never()); + when(delegate.alloc()).thenReturn(allocator); - new FragmentationDuplexConnection(delegate, allocator, 64, false, "").sendOne(encode.retain()); + new FragmentationDuplexConnection(delegate, 64, false, "").sendOne(encode.retain()); verify(delegate).send(publishers.capture()); diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java index c5abce339..013e2ebc2 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java @@ -25,6 +25,7 @@ import io.netty.buffer.CompositeByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.DuplexConnection; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.CancelFrameFlyweight; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; @@ -51,7 +52,8 @@ final class ReassembleDuplexConnectionTest { private final DuplexConnection delegate = mock(DuplexConnection.class, RETURNS_SMART_NULLS); - private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + private LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); @DisplayName("reassembles data") @Test @@ -82,8 +84,9 @@ void reassembleData() { when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); when(delegate.onClose()).thenReturn(Mono.never()); + when(delegate.alloc()).thenReturn(allocator); - new ReassemblyDuplexConnection(delegate, allocator, false) + new ReassemblyDuplexConnection(delegate, false) .receive() .as(StepVerifier::create) .assertNext( @@ -146,8 +149,9 @@ void reassembleMetadata() { when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); when(delegate.onClose()).thenReturn(Mono.never()); + when(delegate.alloc()).thenReturn(allocator); - new ReassemblyDuplexConnection(delegate, allocator, false) + new ReassemblyDuplexConnection(delegate, false) .receive() .as(StepVerifier::create) .assertNext( @@ -213,8 +217,9 @@ void reassembleMetadataAndData() { when(delegate.receive()).thenReturn(Flux.fromIterable(byteBufs)); when(delegate.onClose()).thenReturn(Mono.never()); + when(delegate.alloc()).thenReturn(allocator); - new ReassemblyDuplexConnection(delegate, allocator, false) + new ReassemblyDuplexConnection(delegate, false) .receive() .as(StepVerifier::create) .assertNext( @@ -234,8 +239,9 @@ void reassembleNonFragment() { when(delegate.receive()).thenReturn(Flux.just(encode)); when(delegate.onClose()).thenReturn(Mono.never()); + when(delegate.alloc()).thenReturn(allocator); - new ReassemblyDuplexConnection(delegate, allocator, false) + new ReassemblyDuplexConnection(delegate, false) .receive() .as(StepVerifier::create) .assertNext( @@ -253,8 +259,9 @@ void reassembleNonFragmentableFrame() { when(delegate.receive()).thenReturn(Flux.just(encode)); when(delegate.onClose()).thenReturn(Mono.never()); + when(delegate.alloc()).thenReturn(allocator); - new ReassemblyDuplexConnection(delegate, allocator, false) + new ReassemblyDuplexConnection(delegate, false) .receive() .as(StepVerifier::create) .assertNext( diff --git a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java index 9a4b5d2db..efa962c48 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java @@ -21,6 +21,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.*; import io.rsocket.plugins.InitializingInterceptorRegistry; import io.rsocket.test.util.TestDuplexConnection; @@ -32,12 +33,13 @@ public class ClientServerInputMultiplexerTest { private TestDuplexConnection source; private ClientServerInputMultiplexer clientMultiplexer; - private ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; + private LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); private ClientServerInputMultiplexer serverMultiplexer; @Before public void setup() { - source = new TestDuplexConnection(); + source = new TestDuplexConnection(allocator); clientMultiplexer = new ClientServerInputMultiplexer(source, new InitializingInterceptorRegistry(), true); serverMultiplexer = diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java b/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java index d945dd45d..58323c066 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/LocalDuplexConnection.java @@ -17,6 +17,7 @@ package io.rsocket.test.util; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import org.reactivestreams.Publisher; import reactor.core.publisher.DirectProcessor; @@ -25,17 +26,22 @@ import reactor.core.publisher.MonoProcessor; public class LocalDuplexConnection implements DuplexConnection { + private final ByteBufAllocator allocator; private final DirectProcessor send; private final DirectProcessor receive; private final MonoProcessor onClose; private final String name; public LocalDuplexConnection( - String name, DirectProcessor send, DirectProcessor receive) { + String name, + ByteBufAllocator allocator, + DirectProcessor send, + DirectProcessor receive) { this.name = name; + this.allocator = allocator; this.send = send; this.receive = receive; - onClose = MonoProcessor.create(); + this.onClose = MonoProcessor.create(); } @Override @@ -52,6 +58,11 @@ public Flux receive() { return receive.doOnNext(f -> System.out.println(name + " - " + f.toString())); } + @Override + public ByteBufAllocator alloc() { + return allocator; + } + @Override public void dispose() { onClose.onComplete(); diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java index 37ad8ee5b..a30e75875 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestClientTransport.java @@ -1,12 +1,15 @@ package io.rsocket.test.util; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.transport.ClientTransport; import reactor.core.publisher.Mono; public class TestClientTransport implements ClientTransport { - - private final TestDuplexConnection testDuplexConnection = new TestDuplexConnection(); + private final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + private final TestDuplexConnection testDuplexConnection = new TestDuplexConnection(allocator); @Override public Mono connect(int mtu) { @@ -16,4 +19,8 @@ public Mono connect(int mtu) { public TestDuplexConnection testConnection() { return testDuplexConnection; } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } } diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java index 6298b0c3a..17a19b8c9 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestDuplexConnection.java @@ -17,6 +17,7 @@ package io.rsocket.test.util; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import java.util.Collection; import java.util.concurrent.ConcurrentLinkedQueue; @@ -46,17 +47,19 @@ public class TestDuplexConnection implements DuplexConnection { private final FluxSink receivedSink; private final MonoProcessor onClose; private final ConcurrentLinkedQueue> sendSubscribers; + private final ByteBufAllocator allocator; private volatile double availability = 1; private volatile int initialSendRequestN = Integer.MAX_VALUE; - public TestDuplexConnection() { - sent = new LinkedBlockingQueue<>(); - received = DirectProcessor.create(); - receivedSink = received.sink(); - sentPublisher = DirectProcessor.create(); - sendSink = sentPublisher.sink(); - sendSubscribers = new ConcurrentLinkedQueue<>(); - onClose = MonoProcessor.create(); + public TestDuplexConnection(ByteBufAllocator allocator) { + this.allocator = allocator; + this.sent = new LinkedBlockingQueue<>(); + this.received = DirectProcessor.create(); + this.receivedSink = received.sink(); + this.sentPublisher = DirectProcessor.create(); + this.sendSink = sentPublisher.sink(); + this.sendSubscribers = new ConcurrentLinkedQueue<>(); + this.onClose = MonoProcessor.create(); } @Override @@ -83,6 +86,11 @@ public Flux receive() { return received; } + @Override + public ByteBufAllocator alloc() { + return allocator; + } + @Override public double availability() { return availability; diff --git a/rsocket-core/src/test/java/io/rsocket/test/util/TestServerTransport.java b/rsocket-core/src/test/java/io/rsocket/test/util/TestServerTransport.java index 5cebf0da1..325496148 100644 --- a/rsocket-core/src/test/java/io/rsocket/test/util/TestServerTransport.java +++ b/rsocket-core/src/test/java/io/rsocket/test/util/TestServerTransport.java @@ -1,12 +1,16 @@ package io.rsocket.test.util; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.Closeable; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.transport.ServerTransport; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; public class TestServerTransport implements ServerTransport { private final MonoProcessor conn = MonoProcessor.create(); + private final LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); @Override public Mono start(ConnectionAcceptor acceptor, int mtu) { @@ -39,8 +43,12 @@ private void disposeConnection() { } public TestDuplexConnection connect() { - TestDuplexConnection c = new TestDuplexConnection(); + TestDuplexConnection c = new TestDuplexConnection(allocator); conn.onNext(c); return c; } + + public LeaksTrackingByteBufAllocator alloc() { + return allocator; + } } 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 fd2dcbbd6..24f029845 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 @@ -16,7 +16,6 @@ package io.rsocket.examples.transport.ws; -import io.netty.buffer.ByteBufAllocator; import io.netty.handler.codec.http.HttpResponseStatus; import io.rsocket.AbstractRSocket; import io.rsocket.ConnectionSetupPayload; @@ -65,9 +64,7 @@ public static void main(String[] args) { if (in.headers().containsValue("Authorization", "test", true)) { DuplexConnection connection = new ReassemblyDuplexConnection( - new WebsocketDuplexConnection((Connection) in), - ByteBufAllocator.DEFAULT, - false); + new WebsocketDuplexConnection((Connection) in), false); return acceptor.apply(connection).then(out.neverComplete()); } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java index 20d58dcb7..9904c2b24 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java @@ -20,6 +20,7 @@ import io.micrometer.core.instrument.*; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; @@ -82,6 +83,11 @@ final class MicrometerDuplexConnection implements DuplexConnection { this.frameCounters = new FrameCounters(connectionType, meterRegistry, tags); } + @Override + public ByteBufAllocator alloc() { + return delegate.alloc(); + } + @Override public void dispose() { delegate.dispose(); diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java index 0d6a10391..d69bd65e8 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalClientTransport.java @@ -37,21 +37,39 @@ public final class LocalClientTransport implements ClientTransport { private final String name; - private LocalClientTransport(String name) { + private final ByteBufAllocator allocator; + + private LocalClientTransport(String name, ByteBufAllocator allocator) { this.name = name; + this.allocator = allocator; } /** * Creates a new instance. * - * @param name the name of the {@link ServerTransport} instance to connect to + * @param name the name of the {@link ClientTransport} instance to connect to * @return a new instance * @throws NullPointerException if {@code name} is {@code null} */ public static LocalClientTransport create(String name) { Objects.requireNonNull(name, "name must not be null"); - return new LocalClientTransport(name); + return create(name, ByteBufAllocator.DEFAULT); + } + + /** + * Creates a new instance. + * + * @param name the name of the {@link ClientTransport} instance to connect to + * @param allocator the allocator used by {@link ClientTransport} instance + * @return a new instance + * @throws NullPointerException if {@code name} is {@code null} + */ + public static LocalClientTransport create(String name, ByteBufAllocator allocator) { + Objects.requireNonNull(name, "name must not be null"); + Objects.requireNonNull(allocator, "allocator must not be null"); + + return new LocalClientTransport(name, allocator); } private Mono connect() { @@ -66,9 +84,10 @@ private Mono connect() { UnboundedProcessor out = new UnboundedProcessor<>(); MonoProcessor closeNotifier = MonoProcessor.create(); - server.accept(new LocalDuplexConnection(out, in, closeNotifier)); + server.accept(new LocalDuplexConnection(allocator, out, in, closeNotifier)); - return Mono.just((DuplexConnection) new LocalDuplexConnection(in, out, closeNotifier)); + return Mono.just( + (DuplexConnection) new LocalDuplexConnection(allocator, in, out, closeNotifier)); }); } @@ -80,11 +99,9 @@ public Mono connect(int mtu) { return connect.map( duplexConnection -> { if (mtu > 0) { - return new FragmentationDuplexConnection( - duplexConnection, ByteBufAllocator.DEFAULT, mtu, false, "client"); + return new FragmentationDuplexConnection(duplexConnection, mtu, false, "client"); } else { - return new ReassemblyDuplexConnection( - duplexConnection, ByteBufAllocator.DEFAULT, false); + return new ReassemblyDuplexConnection(duplexConnection, false); } }); } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java index f9501717c..afaa14f95 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalDuplexConnection.java @@ -17,6 +17,7 @@ package io.rsocket.transport.local; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import java.util.Objects; import org.reactivestreams.Publisher; @@ -28,6 +29,7 @@ /** An implementation of {@link DuplexConnection} that connects inside the same JVM. */ final class LocalDuplexConnection implements DuplexConnection { + private final ByteBufAllocator allocator; private final Flux in; private final MonoProcessor onClose; @@ -42,7 +44,12 @@ final class LocalDuplexConnection implements DuplexConnection { * @param onClose the closing notifier * @throws NullPointerException if {@code in}, {@code out}, or {@code onClose} are {@code null} */ - LocalDuplexConnection(Flux in, Subscriber out, MonoProcessor onClose) { + LocalDuplexConnection( + ByteBufAllocator allocator, + Flux in, + Subscriber out, + MonoProcessor onClose) { + this.allocator = Objects.requireNonNull(allocator, "allocator must not be null"); this.in = Objects.requireNonNull(in, "in must not be null"); this.out = Objects.requireNonNull(out, "out must not be null"); this.onClose = Objects.requireNonNull(onClose, "onClose must not be null"); @@ -82,4 +89,9 @@ public Mono sendOne(ByteBuf frame) { out.onNext(frame); return Mono.empty(); } + + @Override + public ByteBufAllocator alloc() { + return allocator; + } } diff --git a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java index 329b4e38c..382b4533a 100644 --- a/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java +++ b/rsocket-transport-local/src/main/java/io/rsocket/transport/local/LocalServerTransport.java @@ -16,7 +16,6 @@ package io.rsocket.transport.local; -import io.netty.buffer.ByteBufAllocator; import io.rsocket.Closeable; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; @@ -167,11 +166,9 @@ public void accept(DuplexConnection duplexConnection) { if (mtu > 0) { duplexConnection = - new FragmentationDuplexConnection( - duplexConnection, ByteBufAllocator.DEFAULT, mtu, false, "server"); + new FragmentationDuplexConnection(duplexConnection, mtu, false, "server"); } else { - duplexConnection = - new ReassemblyDuplexConnection(duplexConnection, ByteBufAllocator.DEFAULT, false); + duplexConnection = new ReassemblyDuplexConnection(duplexConnection, false); } acceptor.apply(duplexConnection).subscribe(); diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java index c9c29f0a9..aa28ab8b9 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java @@ -62,6 +62,11 @@ public TcpDuplexConnection(Connection connection, boolean encodeLength) { }); } + @Override + public ByteBufAllocator alloc() { + return connection.channel().alloc(); + } + @Override protected void doOnClose() { if (!connection.isDisposed()) { diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java index ead297928..0183ef19d 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/WebsocketDuplexConnection.java @@ -16,6 +16,7 @@ package io.rsocket.transport.netty; import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame; import io.rsocket.DuplexConnection; import io.rsocket.internal.BaseDuplexConnection; @@ -53,6 +54,11 @@ public WebsocketDuplexConnection(Connection connection) { }); } + @Override + public ByteBufAllocator alloc() { + return connection.channel().alloc(); + } + @Override protected void doOnClose() { if (!connection.isDisposed()) { diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java index 8059b36bd..8be019f1c 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java @@ -16,7 +16,6 @@ package io.rsocket.transport.netty.client; -import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.fragmentation.ReassemblyDuplexConnection; @@ -105,14 +104,9 @@ public Mono connect(int mtu) { c -> { if (mtu > 0) { return new FragmentationDuplexConnection( - new TcpDuplexConnection(c, false), - ByteBufAllocator.DEFAULT, - mtu, - true, - "client"); + new TcpDuplexConnection(c, false), mtu, true, "client"); } else { - return new ReassemblyDuplexConnection( - new TcpDuplexConnection(c), ByteBufAllocator.DEFAULT, false); + return new ReassemblyDuplexConnection(new TcpDuplexConnection(c), false); } }); } 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 49a2c2e92..b19621d46 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 @@ -20,7 +20,6 @@ import static io.rsocket.transport.netty.UriUtils.getPort; import static io.rsocket.transport.netty.UriUtils.isSecure; -import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.fragmentation.ReassemblyDuplexConnection; @@ -164,11 +163,9 @@ public Mono connect(int mtu) { DuplexConnection connection = new WebsocketDuplexConnection(c); if (mtu > 0) { connection = - new FragmentationDuplexConnection( - connection, ByteBufAllocator.DEFAULT, mtu, false, "client"); + new FragmentationDuplexConnection(connection, mtu, false, "client"); } else { - connection = - new ReassemblyDuplexConnection(connection, ByteBufAllocator.DEFAULT, false); + connection = new ReassemblyDuplexConnection(connection, false); } return connection; }); diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java index d39cc8e67..56dd59d45 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/TcpServerTransport.java @@ -16,7 +16,6 @@ package io.rsocket.transport.netty.server; -import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.fragmentation.ReassemblyDuplexConnection; @@ -106,15 +105,9 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { if (mtu > 0) { connection = new FragmentationDuplexConnection( - new TcpDuplexConnection(c, false), - ByteBufAllocator.DEFAULT, - mtu, - true, - "server"); + new TcpDuplexConnection(c, false), mtu, true, "server"); } else { - connection = - new ReassemblyDuplexConnection( - new TcpDuplexConnection(c), ByteBufAllocator.DEFAULT, false); + connection = new ReassemblyDuplexConnection(new TcpDuplexConnection(c), false); } acceptor .apply(connection) diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java index 1d8769cc6..83cb010b7 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java @@ -18,7 +18,6 @@ import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; -import io.netty.buffer.ByteBufAllocator; import io.rsocket.Closeable; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; @@ -105,11 +104,9 @@ public static BiFunction> n return (in, out) -> { DuplexConnection connection = new WebsocketDuplexConnection((Connection) in); if (mtu > 0) { - connection = - new FragmentationDuplexConnection( - connection, ByteBufAllocator.DEFAULT, mtu, false, "server"); + connection = new FragmentationDuplexConnection(connection, mtu, false, "server"); } else { - connection = new ReassemblyDuplexConnection(connection, ByteBufAllocator.DEFAULT, false); + connection = new ReassemblyDuplexConnection(connection, false); } return acceptor.apply(connection).then(out.neverComplete()); }; diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java index 01c519ea3..4a0331c08 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java @@ -18,7 +18,6 @@ import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; -import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.fragmentation.ReassemblyDuplexConnection; @@ -129,12 +128,9 @@ public Mono start(ConnectionAcceptor acceptor, int mtu) { new WebsocketDuplexConnection((Connection) in); if (mtu > 0) { connection = - new FragmentationDuplexConnection( - connection, ByteBufAllocator.DEFAULT, mtu, false, "server"); + new FragmentationDuplexConnection(connection, mtu, false, "server"); } else { - connection = - new ReassemblyDuplexConnection( - connection, ByteBufAllocator.DEFAULT, false); + connection = new ReassemblyDuplexConnection(connection, false); } return acceptor.apply(connection).then(out.neverComplete()); }, From 414193f1bb8803a0954ae91dd81d869fba9b2eb4 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 26 Apr 2020 15:50:31 +0300 Subject: [PATCH 147/181] Send root-cause errors from connection to onClose of RSocket (#797) Co-Authored-By: Rossen Stoyanchev --- .../src/main/java/io/rsocket/Closeable.java | 14 ++- .../io/rsocket/core/RSocketConnector.java | 2 +- .../io/rsocket/core/RSocketRequester.java | 25 +++-- .../io/rsocket/core/RSocketResponder.java | 93 ++++++++++--------- .../rsocket/resume/ResumeIntegrationTest.java | 29 +----- 5 files changed, 80 insertions(+), 83 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/Closeable.java b/rsocket-core/src/main/java/io/rsocket/Closeable.java index 5eb871e18..2ea9a0371 100644 --- a/rsocket-core/src/main/java/io/rsocket/Closeable.java +++ b/rsocket-core/src/main/java/io/rsocket/Closeable.java @@ -16,17 +16,21 @@ package io.rsocket; +import org.reactivestreams.Subscriber; import reactor.core.Disposable; import reactor.core.publisher.Mono; -/** */ +/** An interface which allows listening to when a specific instance of this interface is closed */ public interface Closeable extends Disposable { /** - * Returns a {@code Publisher} that completes when this {@code RSocket} is closed. A {@code - * RSocket} can be closed by explicitly calling {@link RSocket#dispose()} or when the underlying - * transport connection is closed. + * Returns a {@link Mono} that terminates when the instance is terminated by any reason. Note, in + * case of error termination, the cause of error will be propagated as an error signal through + * {@link org.reactivestreams.Subscriber#onError(Throwable)}. Otherwise, {@link + * Subscriber#onComplete()} will be called. * - * @return A {@code Publisher} that completes when this {@code RSocket} close is complete. + * @return a {@link Mono} to track completion with success or error of the underlying resource. + * When the underlying resource is an `RSocket`, the {@code Mono} exposes stream 0 (i.e. + * connection level) errors. */ Mono onClose(); } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index dc5be2430..146fd599d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -50,7 +50,7 @@ public class RSocketConnector { private static final int MIN_MTU_SIZE = 64; private static final BiConsumer INVALIDATE_FUNCTION = - (r, i) -> r.onClose().subscribe(null, null, i::invalidate); + (r, i) -> r.onClose().subscribe(null, __ -> i.invalidate(), i::invalidate); private Payload setupPayload = EmptyPayload.INSTANCE; private String metadataMimeType = "application/binary"; diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 04c766eec..21112c24d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -53,6 +53,7 @@ import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.util.MonoLifecycleHandler; import java.nio.channels.ClosedChannelException; +import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; @@ -67,6 +68,7 @@ import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.SignalType; import reactor.core.publisher.UnicastProcessor; import reactor.util.concurrent.Queues; @@ -106,6 +108,7 @@ class RSocketRequester implements RSocket { private final ByteBufAllocator allocator; private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; private volatile Throwable terminationError; + private final MonoProcessor onClose; RSocketRequester( DuplexConnection connection, @@ -126,14 +129,15 @@ class RSocketRequester implements RSocket { this.leaseHandler = leaseHandler; this.senders = new SynchronizedIntObjectHashMap<>(); this.receivers = new SynchronizedIntObjectHashMap<>(); + this.onClose = MonoProcessor.create(); // DO NOT Change the order here. The Send processor must be subscribed to before receiving this.sendProcessor = new UnboundedProcessor<>(); connection .onClose() - .doFinally(signalType -> tryTerminateOnConnectionClose()) - .subscribe(null, errorConsumer); + .or(onClose) + .subscribe(null, this::tryTerminateOnConnectionError, this::tryTerminateOnConnectionClose); connection.send(sendProcessor).subscribe(null, this::handleSendProcessorError); connection.receive().subscribe(this::handleIncomingFrames, errorConsumer); @@ -181,17 +185,17 @@ public double availability() { @Override public void dispose() { - connection.dispose(); + tryTerminate(() -> new CancellationException("Disposed")); } @Override public boolean isDisposed() { - return connection.isDisposed(); + return onClose.isDisposed(); } @Override public Mono onClose() { - return connection.onClose(); + return onClose; } private Mono handleFireAndForget(Payload payload) { @@ -619,6 +623,10 @@ private void tryTerminateOnKeepAlive(KeepAlive keepAlive) { String.format("No keep-alive acks for %d ms", keepAlive.getTimeout().toMillis()))); } + private void tryTerminateOnConnectionError(Throwable e) { + tryTerminate(() -> e); + } + private void tryTerminateOnConnectionClose() { tryTerminate(() -> CLOSED_CHANNEL_EXCEPTION); } @@ -627,16 +635,16 @@ private void tryTerminateOnZeroError(ByteBuf errorFrame) { tryTerminate(() -> Exceptions.from(0, errorFrame)); } - private void tryTerminate(Supplier errorSupplier) { + private void tryTerminate(Supplier errorSupplier) { if (terminationError == null) { - Exception e = errorSupplier.get(); + Throwable e = errorSupplier.get(); if (TERMINATION_ERROR.compareAndSet(this, null, e)) { terminate(e); } } } - private void terminate(Exception e) { + private void terminate(Throwable e) { connection.dispose(); leaseHandler.dispose(); @@ -668,6 +676,7 @@ private void terminate(Exception e) { receivers.clear(); sendProcessor.dispose(); errorConsumer.accept(e); + onClose.onError(e); } private void removeStreamReceiver(int streamId) { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index f992b7577..b5b298e14 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -34,8 +34,12 @@ import io.rsocket.internal.SynchronizedIntObjectHashMap; import io.rsocket.internal.UnboundedProcessor; import io.rsocket.lease.ResponderLeaseHandler; +import java.nio.channels.ClosedChannelException; +import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; import java.util.function.LongConsumer; +import java.util.function.Supplier; import javax.annotation.Nullable; import org.reactivestreams.Processor; import org.reactivestreams.Publisher; @@ -58,6 +62,7 @@ class RSocketResponder implements ResponderRSocket { } } }; + private static final Exception CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); private final DuplexConnection connection; private final RSocket requestHandler; @@ -65,6 +70,13 @@ class RSocketResponder implements ResponderRSocket { private final PayloadDecoder payloadDecoder; private final Consumer errorConsumer; private final ResponderLeaseHandler leaseHandler; + private final Disposable leaseHandlerDisposable; + private final MonoProcessor onClose; + + private volatile Throwable terminationError; + private static final AtomicReferenceFieldUpdater TERMINATION_ERROR = + AtomicReferenceFieldUpdater.newUpdater( + RSocketResponder.class, Throwable.class, "terminationError"); private final int mtu; @@ -94,28 +106,21 @@ class RSocketResponder implements ResponderRSocket { this.leaseHandler = leaseHandler; this.sendingSubscriptions = new SynchronizedIntObjectHashMap<>(); this.channelProcessors = new SynchronizedIntObjectHashMap<>(); + this.onClose = MonoProcessor.create(); // DO NOT Change the order here. The Send processor must be subscribed to before receiving // connections this.sendProcessor = new UnboundedProcessor<>(); - connection - .send(sendProcessor) - .doFinally(this::handleSendProcessorCancel) - .subscribe(null, this::handleSendProcessorError); + connection.send(sendProcessor).subscribe(null, this::handleSendProcessorError); - Disposable receiveDisposable = connection.receive().subscribe(this::handleFrame, errorConsumer); - Disposable sendLeaseDisposable = leaseHandler.send(sendProcessor::onNextPrioritized); + connection.receive().subscribe(this::handleFrame, errorConsumer); + leaseHandlerDisposable = leaseHandler.send(sendProcessor::onNextPrioritized); this.connection .onClose() - .doFinally( - s -> { - cleanup(); - receiveDisposable.dispose(); - sendLeaseDisposable.dispose(); - }) - .subscribe(null, errorConsumer); + .or(onClose) + .subscribe(null, this::tryTerminateOnConnectionError, this::tryTerminateOnConnectionClose); } private void handleSendProcessorError(Throwable t) { @@ -142,32 +147,21 @@ private void handleSendProcessorError(Throwable t) { }); } - private void handleSendProcessorCancel(SignalType t) { - if (SignalType.ON_ERROR == t) { - return; - } + private void tryTerminateOnConnectionError(Throwable e) { + tryTerminate(() -> e); + } - sendingSubscriptions - .values() - .forEach( - subscription -> { - try { - subscription.cancel(); - } catch (Throwable e) { - errorConsumer.accept(e); - } - }); + private void tryTerminateOnConnectionClose() { + tryTerminate(() -> CLOSED_CHANNEL_EXCEPTION); + } - channelProcessors - .values() - .forEach( - subscription -> { - try { - subscription.onComplete(); - } catch (Throwable e) { - errorConsumer.accept(e); - } - }); + private void tryTerminate(Supplier errorSupplier) { + if (terminationError == null) { + Throwable e = errorSupplier.get(); + if (TERMINATION_ERROR.compareAndSet(this, null, e)) { + cleanup(e); + } + } } @Override @@ -250,23 +244,25 @@ public Mono metadataPush(Payload payload) { @Override public void dispose() { - connection.dispose(); + tryTerminate(() -> new CancellationException("Disposed")); } @Override public boolean isDisposed() { - return connection.isDisposed(); + return onClose.isDisposed(); } @Override public Mono onClose() { - return connection.onClose(); + return onClose; } - private void cleanup() { + private void cleanup(Throwable e) { cleanUpSendingSubscriptions(); - cleanUpChannelProcessors(); + cleanUpChannelProcessors(e); + connection.dispose(); + leaseHandlerDisposable.dispose(); requestHandler.dispose(); sendProcessor.dispose(); } @@ -276,8 +272,17 @@ private synchronized void cleanUpSendingSubscriptions() { sendingSubscriptions.clear(); } - private synchronized void cleanUpChannelProcessors() { - channelProcessors.values().forEach(Processor::onComplete); + private synchronized void cleanUpChannelProcessors(Throwable e) { + channelProcessors + .values() + .forEach( + payloadPayloadProcessor -> { + try { + payloadPayloadProcessor.onError(e); + } catch (Throwable t) { + // noops + } + }); channelProcessors.clear(); } diff --git a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java index 685cef41e..6ae870a15 100644 --- a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java @@ -35,13 +35,11 @@ import java.nio.channels.ClosedChannelException; import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; -import java.util.function.Consumer; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.ReplayProcessor; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; @@ -105,7 +103,6 @@ public void reconnectOnMissingSession() { DisconnectableClientTransport clientTransport = new DisconnectableClientTransport(clientTransport(closeable.address())); - ErrorConsumer errorConsumer = new ErrorConsumer(); int clientSessionDurationSeconds = 10; RSocket rSocket = newClientRSocket(clientTransport, clientSessionDurationSeconds).block(); @@ -118,12 +115,11 @@ public void reconnectOnMissingSession() { .expectError() .verify(Duration.ofSeconds(5)); - StepVerifier.create(errorConsumer.errors().next()) - .expectNextMatches( + StepVerifier.create(rSocket.onClose()) + .expectErrorMatches( err -> err instanceof RejectedResumeException && "unknown resume token".equals(err.getMessage())) - .expectComplete() .verify(Duration.ofSeconds(5)); } @@ -134,23 +130,19 @@ void serverMissingResume() { .bind(serverTransport(SERVER_HOST, SERVER_PORT)) .block(); - ErrorConsumer errorConsumer = new ErrorConsumer(); - RSocket rSocket = RSocketConnector.create() .resume(new Resume()) .connect(clientTransport(closeableChannel.address())) .block(); - StepVerifier.create(errorConsumer.errors().next().doFinally(s -> closeableChannel.dispose())) - .expectNextMatches( + StepVerifier.create(rSocket.onClose().doFinally(s -> closeableChannel.dispose())) + .expectErrorMatches( err -> err instanceof UnsupportedSetupException && "resume not supported".equals(err.getMessage())) - .expectComplete() .verify(Duration.ofSeconds(5)); - StepVerifier.create(rSocket.onClose()).expectComplete().verify(Duration.ofSeconds(5)); Assertions.assertThat(rSocket.isDisposed()).isTrue(); } @@ -162,19 +154,6 @@ static ServerTransport serverTransport(String host, int port) return TcpServerTransport.create(host, port); } - private static class ErrorConsumer implements Consumer { - private final ReplayProcessor errors = ReplayProcessor.create(); - - public Flux errors() { - return errors; - } - - @Override - public void accept(Throwable throwable) { - errors.onNext(throwable); - } - } - private static Flux testRequest() { return Flux.interval(Duration.ofMillis(50)) .map(v -> DefaultPayload.create("client_request")) From 00acd1edcca0cd3666841b53e8933c0de0ae2a2c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 27 Apr 2020 09:09:05 +0100 Subject: [PATCH 148/181] Deprecate ResumeStrategy and replace with Reactor Retry (#798) --- .../main/java/io/rsocket/RSocketFactory.java | 11 ++++- .../io/rsocket/core/RSocketConnector.java | 27 +++++------ .../src/main/java/io/rsocket/core/Resume.java | 30 +++++++----- .../rsocket/resume/ClientRSocketSession.java | 46 ++++++++++++------- .../ExponentialBackoffResumeStrategy.java | 6 +++ .../resume/PeriodicResumeStrategy.java | 6 +++ .../io/rsocket/resume/ResumeStrategy.java | 8 +++- .../tcp/resume/ResumeFileTransfer.java | 26 +++-------- .../rsocket/resume/ResumeIntegrationTest.java | 5 +- 9 files changed, 96 insertions(+), 69 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 43d344b9d..f36a84241 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -17,6 +17,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.core.Resume; @@ -26,6 +27,7 @@ import io.rsocket.plugins.DuplexConnectionInterceptor; import io.rsocket.plugins.RSocketInterceptor; import io.rsocket.plugins.SocketAcceptorInterceptor; +import io.rsocket.resume.ClientResume; import io.rsocket.resume.ResumableFramesStore; import io.rsocket.resume.ResumeStrategy; import io.rsocket.transport.ClientTransport; @@ -97,6 +99,9 @@ default Start transport(ServerTransport transport) { /** Factory to create and configure an RSocket client, and connect to a server. */ public static class ClientRSocketFactory implements ClientTransportAcceptor { + private static final ClientResume CLIENT_RESUME = + new ClientResume(Duration.ofMinutes(2), Unpooled.EMPTY_BUFFER); + private final RSocketConnector connector; private Duration tickPeriod = Duration.ofSeconds(20); @@ -348,9 +353,11 @@ public ClientRSocketFactory resumeStreamTimeout(Duration streamTimeout) { return this; } - public ClientRSocketFactory resumeStrategy(Supplier resumeStrategy) { + public ClientRSocketFactory resumeStrategy(Supplier strategy) { resume(); - resume.resumeStrategy(resumeStrategy); + resume.retry( + Retry.from( + signals -> signals.flatMap(s -> strategy.get().apply(CLIENT_RESUME, s.failure())))); return this; } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 146fd599d..ed20f892e 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -254,8 +254,17 @@ public Mono connect(Supplier transportSupplier) { DuplexConnection wrappedConnection; if (resume != null) { - ClientRSocketSession session = createSession(connection, connectionMono); - resumeToken = session.token(); + resumeToken = resume.getTokenSupplier().get(); + ClientRSocketSession session = + new ClientRSocketSession( + connection, + resume.getSessionDuration(), + resume.getRetry(), + resume.getStoreFactory(CLIENT_TAG).apply(resumeToken), + resume.getStreamTimeout(), + resume.isCleanupStoreOnKeepAlive()) + .continueWith(connectionMono) + .resumeToken(resumeToken); keepAliveHandler = new KeepAliveHandler.ResumableKeepAliveHandler(session.resumableConnection()); wrappedConnection = session.resumableConnection(); @@ -343,18 +352,4 @@ public Mono connect(Supplier transportSupplier) { } }); } - - private ClientRSocketSession createSession( - DuplexConnection connection, Mono connectionMono) { - ByteBuf resumeToken = resume.getTokenSupplier().get(); - ClientRSocketSession session = - new ClientRSocketSession( - connection, - resume.getSessionDuration(), - resume.getResumeStrategySupplier(), - resume.getStoreFactory(CLIENT_TAG).apply(resumeToken), - resume.getStreamTimeout(), - resume.isCleanupStoreOnKeepAlive()); - return session.continueWith(connectionMono).resumeToken(resumeToken); - } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/Resume.java b/rsocket-core/src/main/java/io/rsocket/core/Resume.java index 4b0edab00..aedcc9e5e 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/Resume.java +++ b/rsocket-core/src/main/java/io/rsocket/core/Resume.java @@ -17,23 +17,29 @@ import io.netty.buffer.ByteBuf; import io.rsocket.frame.ResumeFrameFlyweight; -import io.rsocket.resume.ExponentialBackoffResumeStrategy; import io.rsocket.resume.InMemoryResumableFramesStore; import io.rsocket.resume.ResumableFramesStore; -import io.rsocket.resume.ResumeStrategy; import java.time.Duration; import java.util.function.Function; import java.util.function.Supplier; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.util.retry.Retry; public class Resume { - Duration sessionDuration = Duration.ofMinutes(2); - Duration streamTimeout = Duration.ofSeconds(10); - boolean cleanupStoreOnKeepAlive; - Function storeFactory; + private static final Logger logger = LoggerFactory.getLogger(Resume.class); + + private Duration sessionDuration = Duration.ofMinutes(2); + private Duration streamTimeout = Duration.ofSeconds(10); + private boolean cleanupStoreOnKeepAlive; + private Function storeFactory; private Supplier tokenSupplier = ResumeFrameFlyweight::generateResumeToken; - private Supplier resumeStrategySupplier = - () -> new ExponentialBackoffResumeStrategy(Duration.ofSeconds(1), Duration.ofSeconds(16), 2); + private Retry retry = + Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(1)) + .maxBackoff(Duration.ofSeconds(16)) + .jitter(1.0) + .doBeforeRetry(signal -> logger.debug("Connection error", signal.failure())); public Resume() {} @@ -63,8 +69,8 @@ public Resume token(Supplier supplier) { return this; } - public Resume resumeStrategy(Supplier supplier) { - this.resumeStrategySupplier = supplier; + public Resume retry(Retry retry) { + this.retry = retry; return this; } @@ -90,7 +96,7 @@ Supplier getTokenSupplier() { return tokenSupplier; } - Supplier getResumeStrategySupplier() { - return resumeStrategySupplier; + Retry getRetry() { + return retry; } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java index 509fb5168..01b6dfeae 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * 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. @@ -26,10 +26,11 @@ import io.rsocket.internal.ClientServerInputMultiplexer; import java.time.Duration; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; public class ClientRSocketSession implements RSocketSession> { private static final Logger logger = LoggerFactory.getLogger(ClientRSocketSession.class); @@ -42,7 +43,7 @@ public class ClientRSocketSession implements RSocketSession resumeStrategy, + Retry retry, ResumableFramesStore resumableFramesStore, Duration resumeStreamTimeout, boolean cleanupStoreOnKeepAlive) { @@ -63,24 +64,13 @@ public ClientRSocketSession( .flatMap( err -> { logger.debug("Client session connection error. Starting new connection"); - ResumeStrategy reconnectOnError = resumeStrategy.get(); - ClientResume clientResume = new ClientResume(resumeSessionDuration, resumeToken); AtomicBoolean once = new AtomicBoolean(); return newConnection .delaySubscription( once.compareAndSet(false, true) - ? reconnectOnError.apply(clientResume, err) + ? retry.generateCompanion(Flux.just(new RetrySignal(err))) : Mono.empty()) - .retryWhen( - errors -> - errors - .doOnNext( - retryErr -> - logger.debug("Resumption reconnection error", retryErr)) - .flatMap( - retryErr -> - Mono.from(reconnectOnError.apply(clientResume, retryErr)) - .doOnNext(v -> logger.debug("Retrying with: {}", v)))) + .retryWhen(retry) .timeout(resumeSessionDuration); }) .map(ClientServerInputMultiplexer::new) @@ -177,4 +167,28 @@ private static long remotePos(ByteBuf resumeOkFrame) { private static ConnectionErrorException errorFrameThrowable(long impliedPos) { return new ConnectionErrorException("resumption_server_pos=[" + impliedPos + "]"); } + + private static class RetrySignal implements Retry.RetrySignal { + + private final Throwable ex; + + RetrySignal(Throwable ex) { + this.ex = ex; + } + + @Override + public long totalRetries() { + return 0; + } + + @Override + public long totalRetriesInARow() { + return 0; + } + + @Override + public Throwable failure() { + return ex; + } + } } diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ExponentialBackoffResumeStrategy.java b/rsocket-core/src/main/java/io/rsocket/resume/ExponentialBackoffResumeStrategy.java index b46ac864b..461be02d2 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ExponentialBackoffResumeStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ExponentialBackoffResumeStrategy.java @@ -21,7 +21,13 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; +/** + * @deprecated as of 1.0 RC7 in favor of passing {@link Retry#backoff(long, Duration)} to {@link + * io.rsocket.core.Resume#retry(Retry)}. + */ +@Deprecated public class ExponentialBackoffResumeStrategy implements ResumeStrategy { private volatile Duration next; private final Duration firstBackoff; diff --git a/rsocket-core/src/main/java/io/rsocket/resume/PeriodicResumeStrategy.java b/rsocket-core/src/main/java/io/rsocket/resume/PeriodicResumeStrategy.java index abfefe0b1..bd447c8a9 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/PeriodicResumeStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/PeriodicResumeStrategy.java @@ -19,7 +19,13 @@ import java.time.Duration; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; +/** + * @deprecated as of 1.0 RC7 in favor of passing {@link Retry#fixedDelay(long, Duration)} to {@link + * io.rsocket.core.Resume#retry(Retry)}. + */ +@Deprecated public class PeriodicResumeStrategy implements ResumeStrategy { private final Duration interval; diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java index 903431192..d9dec9f54 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumeStrategy.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * 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. @@ -18,6 +18,12 @@ import java.util.function.BiFunction; import org.reactivestreams.Publisher; +import reactor.util.retry.Retry; +/** + * @deprecated as of 1.0 RC7 in favor of using {@link io.rsocket.core.Resume#retry(Retry)} via + * {@link io.rsocket.core.RSocketConnector} or {@link io.rsocket.core.RSocketServer}. + */ +@Deprecated @FunctionalInterface public interface ResumeStrategy extends BiFunction> {} 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 3cae64409..d8dd3dd2d 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,17 +22,14 @@ import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.core.Resume; -import io.rsocket.resume.ClientResume; -import io.rsocket.resume.PeriodicResumeStrategy; -import io.rsocket.resume.ResumeStrategy; 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 org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.retry.Retry; public class ResumeFileTransfer { /*amount of file chunks requested by subscriber: n, refilled on n/2 of received items*/ @@ -43,8 +40,11 @@ public static void main(String[] args) { Resume resume = new Resume() .sessionDuration(Duration.ofMinutes(5)) - .resumeStrategy( - () -> new VerboseResumeStrategy(new PeriodicResumeStrategy(Duration.ofSeconds(1)))); + .retry( + Retry.fixedDelay(Long.MAX_VALUE, Duration.ofSeconds(1)) + .doBeforeRetry( + retrySignal -> + System.out.println("Disconnected. Trying to resume connection..."))); CloseableChannel server = RSocketServer.create((setup, rSocket) -> Mono.just(new FileServer(requestCodec))) @@ -88,20 +88,6 @@ public Flux requestStream(Payload payload) { } } - private static class VerboseResumeStrategy implements ResumeStrategy { - private final ResumeStrategy resumeStrategy; - - public VerboseResumeStrategy(ResumeStrategy resumeStrategy) { - this.resumeStrategy = resumeStrategy; - } - - @Override - public Publisher apply(ClientResume clientResume, Throwable throwable) { - return Flux.from(resumeStrategy.apply(clientResume, throwable)) - .doOnNext(v -> System.out.println("Disconnected. Trying to resume connection...")); - } - } - private static class RequestCodec { public Payload encode(Request request) { diff --git a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java index 6ae870a15..bd2db39c7 100644 --- a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java @@ -42,6 +42,7 @@ import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.test.StepVerifier; +import reactor.util.retry.Retry; @SlowTest public class ResumeIntegrationTest { @@ -155,7 +156,7 @@ static ServerTransport serverTransport(String host, int port) } private static Flux testRequest() { - return Flux.interval(Duration.ofMillis(50)) + return Flux.interval(Duration.ofMillis(500)) .map(v -> DefaultPayload.create("client_request")) .onBackpressureDrop(); } @@ -183,7 +184,7 @@ private static Mono newClientRSocket( .sessionDuration(Duration.ofSeconds(sessionDurationSeconds)) .storeFactory(t -> new InMemoryResumableFramesStore("client", 500_000)) .cleanupStoreOnKeepAlive() - .resumeStrategy(() -> new PeriodicResumeStrategy(Duration.ofSeconds(1)))) + .retry(Retry.fixedDelay(Long.MAX_VALUE, Duration.ofSeconds(1)))) .keepAlive(Duration.ofSeconds(5), Duration.ofMinutes(5)) .connect(clientTransport); } From d5e8a45bbb6536653a766fcc1400e49350c4596a Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 27 Apr 2020 13:07:39 +0300 Subject: [PATCH 149/181] Fixes possible elements' leaks occurrence (#799) --- .../rsocket/internal/UnboundedProcessor.java | 100 ++++++++++++------ .../resume/ResumableDuplexConnection.java | 4 + 2 files changed, 70 insertions(+), 34 deletions(-) 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 fe664e843..cb8b5d63d 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnboundedProcessor.java @@ -43,28 +43,43 @@ public final class UnboundedProcessor extends FluxProcessor implements Fuseable.QueueSubscription, Fuseable { + final Queue queue; + final Queue priorityQueue; + + volatile boolean done; + Throwable error; + // important to not loose the downstream too early and miss discard hook, while + // having relevant hasDownstreams() + boolean hasDownstream; + volatile CoreSubscriber actual; + + volatile boolean cancelled; + + volatile int once; + @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater ONCE = AtomicIntegerFieldUpdater.newUpdater(UnboundedProcessor.class, "once"); + volatile int wip; + @SuppressWarnings("rawtypes") static final AtomicIntegerFieldUpdater WIP = AtomicIntegerFieldUpdater.newUpdater(UnboundedProcessor.class, "wip"); + volatile int discardGuard; + + @SuppressWarnings("rawtypes") + static final AtomicIntegerFieldUpdater DISCARD_GUARD = + AtomicIntegerFieldUpdater.newUpdater(UnboundedProcessor.class, "discardGuard"); + + volatile long requested; + @SuppressWarnings("rawtypes") static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater.newUpdater(UnboundedProcessor.class, "requested"); - final Queue queue; - final Queue priorityQueue; - volatile boolean done; - Throwable error; - volatile CoreSubscriber actual; - volatile boolean cancelled; - volatile int once; - volatile int wip; - volatile long requested; - volatile boolean outputFused; + boolean outputFused; public UnboundedProcessor() { this.queue = new MpscUnboundedArrayQueue<>(Queues.SMALL_BUFFER_SIZE); @@ -79,6 +94,7 @@ public int getBufferSize() { @Override public Object scanUnsafe(Attr key) { if (Attr.BUFFERED == key) return queue.size(); + if (Attr.PREFETCH == key) return Integer.MAX_VALUE; return super.scanUnsafe(key); } @@ -143,8 +159,8 @@ void drainFused(Subscriber a) { for (; ; ) { if (cancelled) { - clear(); - actual = null; + this.clear(); + hasDownstream = false; return; } @@ -153,7 +169,7 @@ void drainFused(Subscriber a) { a.onNext(null); if (d) { - actual = null; + hasDownstream = false; Throwable ex = error; if (ex != null) { @@ -173,6 +189,9 @@ void drainFused(Subscriber a) { public void drain() { if (WIP.getAndIncrement(this) != 0) { + if (cancelled) { + this.clear(); + } return; } @@ -199,13 +218,13 @@ public void drain() { boolean checkTerminated(boolean d, boolean empty, Subscriber a) { if (cancelled) { - clear(); - actual = null; + this.clear(); + hasDownstream = false; return true; } if (d && empty) { Throwable e = error; - actual = null; + hasDownstream = false; if (e != null) { a.onError(e); } else { @@ -226,10 +245,6 @@ public void onSubscribe(Subscription s) { } } - public long available() { - return requested; - } - @Override public int getPrefetch() { return Integer.MAX_VALUE; @@ -308,7 +323,7 @@ public void subscribe(CoreSubscriber actual) { actual.onSubscribe(this); this.actual = actual; if (cancelled) { - this.actual = null; + this.hasDownstream = false; } else { drain(); } @@ -335,8 +350,8 @@ public void cancel() { cancelled = true; if (WIP.getAndIncrement(this) == 0) { - clear(); - actual = null; + this.clear(); + hasDownstream = false; } } @@ -362,16 +377,29 @@ public boolean isEmpty() { @Override public void clear() { - while (!queue.isEmpty()) { - T t = queue.poll(); - if (t != null) { - release(t); - } + if (DISCARD_GUARD.getAndIncrement(this) != 0) { + return; } - while (!priorityQueue.isEmpty()) { - T t = priorityQueue.poll(); - if (t != null) { - release(t); + + int missed = 1; + + for (; ; ) { + while (!queue.isEmpty()) { + T t = queue.poll(); + if (t != null) { + release(t); + } + } + while (!priorityQueue.isEmpty()) { + T t = priorityQueue.poll(); + if (t != null) { + release(t); + } + } + + missed = DISCARD_GUARD.addAndGet(this, -missed); + if (missed == 0) { + break; } } } @@ -413,14 +441,18 @@ public long downstreamCount() { @Override public boolean hasDownstreams() { - return actual != null; + return hasDownstream; } void release(T t) { if (t instanceof ReferenceCounted) { ReferenceCounted refCounted = (ReferenceCounted) t; if (refCounted.refCnt() > 0) { - refCounted.release(); + try { + refCounted.release(); + } catch (Throwable ex) { + // no ops + } } } } 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 b9c93f4cd..980de2de1 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -223,6 +223,10 @@ public boolean isDisposed() { } private void sendFrame(ByteBuf f) { + if (disposed.get()) { + f.release(); + return; + } /*resuming from store so no need to save again*/ if (state != State.RESUME && isResumableFrame(f)) { resumeSaveFrames.onNext(f); From 97bf015786fe07e0850ba486b5505cb7d949e6a3 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 27 Apr 2020 13:50:28 +0300 Subject: [PATCH 150/181] adds how to access snapshots Signed-off-by: Oleh Dokuka --- README.md | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bb427cc35..538587154 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,26 @@ Releases are available via Maven Central. Example: ```groovy +repositories { + mavenCentral() +} dependencies { implementation 'io.rsocket:rsocket-core:1.0.0-RC6' implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC6' -// implementation 'io.rsocket:rsocket-core:1.0.0-RC4-SNAPSHOT' -// implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC4-SNAPSHOT' +} +``` + +Snapshots are available via [oss.jfrog.org](oss.jfrog.org) (OJO). + +Example: + +```groovy +repositories { + maven { url 'https://oss.jfrog.org/oss-snapshot-local' } +} +dependencies { + implementation 'io.rsocket:rsocket-core:1.0.0-RC7-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC7-SNAPSHOT' } ``` From 482c0c4296e75d540c087ec24d8d8fd2e6a02c1a Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 27 Apr 2020 12:55:51 +0100 Subject: [PATCH 151/181] Fix logging in examples and load-balancer tests (#800) --- rsocket-examples/build.gradle | 2 +- .../tcp/channel/ChannelEchoClient.java | 6 +++- .../tcp/requestresponse/HelloWorldClient.java | 34 +++++++----------- .../examples/transport/tcp/resume/Files.java | 13 +++++-- .../tcp/resume/ResumeFileTransfer.java | 6 +++- .../transport/tcp/stream/StreamingClient.java | 6 +++- .../src/main/resources/log4j.properties | 20 ----------- .../src/main/resources/logback.xml | 35 +++++++++++++++++++ .../src/test/resources/log4j.properties | 21 ----------- .../src/test/resources/logback-test.xml | 33 +++++++++++++++++ rsocket-load-balancer/build.gradle | 1 + .../src/test/resources/log4j.properties | 20 ----------- .../src/test/resources/logback-test.xml | 33 +++++++++++++++++ 13 files changed, 141 insertions(+), 89 deletions(-) delete mode 100644 rsocket-examples/src/main/resources/log4j.properties create mode 100644 rsocket-examples/src/main/resources/logback.xml delete mode 100644 rsocket-examples/src/test/resources/log4j.properties create mode 100644 rsocket-examples/src/test/resources/logback-test.xml delete mode 100644 rsocket-load-balancer/src/test/resources/log4j.properties create mode 100644 rsocket-load-balancer/src/test/resources/logback-test.xml diff --git a/rsocket-examples/build.gradle b/rsocket-examples/build.gradle index 5f63b0761..01e80cfa1 100644 --- a/rsocket-examples/build.gradle +++ b/rsocket-examples/build.gradle @@ -22,13 +22,13 @@ dependencies { implementation project(':rsocket-core') implementation project(':rsocket-transport-local') implementation project(':rsocket-transport-netty') + runtimeOnly 'ch.qos.logback:logback-classic' testImplementation project(':rsocket-test') testImplementation 'org.junit.jupiter:junit-jupiter-api' testImplementation 'org.mockito:mockito-core' testImplementation 'org.assertj:assertj-core' testImplementation 'io.projectreactor:reactor-test' - testImplementation 'ch.qos.logback:logback-classic' // TODO: Remove after JUnit5 migration testCompileOnly 'junit:junit' 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 44daf8c67..71e48790f 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 @@ -28,11 +28,15 @@ 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; import reactor.core.publisher.Mono; public final class ChannelEchoClient { + private static final Logger logger = LoggerFactory.getLogger(ChannelEchoClient.class); + public static void main(String[] args) { RSocketServer.create(new EchoAcceptor()) .bind(TcpServerTransport.create("localhost", 7000)) @@ -45,7 +49,7 @@ public static void main(String[] args) { .requestChannel( Flux.interval(Duration.ofMillis(1000)).map(i -> DefaultPayload.create("Hello"))) .map(Payload::getDataUtf8) - .doOnNext(System.out::println) + .doOnNext(logger::debug) .take(10) .doFinally(signalType -> socket.dispose()) .then() 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 26a79c4e1..1b9994c2f 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 @@ -24,10 +24,14 @@ import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.TcpServerTransport; 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) { RSocketServer.create( (setupPayload, reactiveSocket) -> @@ -39,7 +43,7 @@ public static void main(String[] args) { public Mono requestResponse(Payload p) { if (fail) { fail = false; - return Mono.error(new Throwable()); + return Mono.error(new Throwable("Simulated error")); } else { return Mono.just(p); } @@ -51,26 +55,14 @@ public Mono requestResponse(Payload p) { RSocket socket = RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000)).block(); - socket - .requestResponse(DefaultPayload.create("Hello")) - .map(Payload::getDataUtf8) - .onErrorReturn("error") - .doOnNext(System.out::println) - .block(); - - socket - .requestResponse(DefaultPayload.create("Hello")) - .map(Payload::getDataUtf8) - .onErrorReturn("error") - .doOnNext(System.out::println) - .block(); - - socket - .requestResponse(DefaultPayload.create("Hello")) - .map(Payload::getDataUtf8) - .onErrorReturn("error") - .doOnNext(System.out::println) - .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/tcp/resume/Files.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/resume/Files.java index e6867f8b5..6724ca93f 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/tcp/resume/Files.java @@ -3,13 +3,21 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.Payload; -import java.io.*; +import java.io.BufferedInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.SynchronousSink; class Files { + private static final Logger logger = LoggerFactory.getLogger(Files.class); public static Flux fileSource(String fileName, int chunkSizeBytes) { return Flux.generate( @@ -35,8 +43,7 @@ public void onNext(Payload payload) { ByteBuf data = payload.data(); receivedBytes += data.readableBytes(); receivedCount += 1; - System.out.println( - "Received file chunk: " + receivedCount + ". Total size: " + receivedBytes); + logger.debug("Received file chunk: " + receivedCount + ". Total size: " + receivedBytes); if (outputStream == null) { outputStream = open(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 d8dd3dd2d..d449dd205 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 @@ -27,13 +27,17 @@ import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.retry.Retry; public class ResumeFileTransfer { + /*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 Logger logger = LoggerFactory.getLogger(ResumeFileTransfer.class); public static void main(String[] args) { RequestCodec requestCodec = new RequestCodec(); @@ -44,7 +48,7 @@ public static void main(String[] args) { Retry.fixedDelay(Long.MAX_VALUE, Duration.ofSeconds(1)) .doBeforeRetry( retrySignal -> - System.out.println("Disconnected. Trying to resume connection..."))); + logger.debug("Disconnected. Trying to resume connection..."))); CloseableChannel server = RSocketServer.create((setup, rSocket) -> Mono.just(new FileServer(requestCodec))) diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java index 5f7c777db..1ef2b7a90 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java @@ -27,11 +27,15 @@ import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; import java.time.Duration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public final class StreamingClient { + private static final Logger logger = LoggerFactory.getLogger(StreamingClient.class); + public static void main(String[] args) { RSocketServer.create(new SocketAcceptorImpl()) .bind(TcpServerTransport.create("localhost", 7000)) @@ -43,7 +47,7 @@ public static void main(String[] args) { socket .requestStream(DefaultPayload.create("Hello")) .map(Payload::getDataUtf8) - .doOnNext(System.out::println) + .doOnNext(logger::debug) .take(10) .then() .doFinally(signalType -> socket.dispose()) diff --git a/rsocket-examples/src/main/resources/log4j.properties b/rsocket-examples/src/main/resources/log4j.properties deleted file mode 100644 index 035f18ebd..000000000 --- a/rsocket-examples/src/main/resources/log4j.properties +++ /dev/null @@ -1,20 +0,0 @@ -# -# Copyright 2015-2018 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. -# -log4j.rootLogger=DEBUG, stdout - -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] %c{1} - %m%n \ No newline at end of file diff --git a/rsocket-examples/src/main/resources/logback.xml b/rsocket-examples/src/main/resources/logback.xml new file mode 100644 index 000000000..17dd8b5e3 --- /dev/null +++ b/rsocket-examples/src/main/resources/logback.xml @@ -0,0 +1,35 @@ + + + + + + + + %d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] %c{1} - %m%n + + + + + + + + + + + + + diff --git a/rsocket-examples/src/test/resources/log4j.properties b/rsocket-examples/src/test/resources/log4j.properties deleted file mode 100644 index 51731fc15..000000000 --- a/rsocket-examples/src/test/resources/log4j.properties +++ /dev/null @@ -1,21 +0,0 @@ -# -# Copyright 2015-2018 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. -# -log4j.rootLogger=INFO, stdout - -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss,SSS} %5p [%t] (%F) - %m%n -#log4j.logger.io.rsocket.FrameLogger=Debug \ No newline at end of file diff --git a/rsocket-examples/src/test/resources/logback-test.xml b/rsocket-examples/src/test/resources/logback-test.xml new file mode 100644 index 000000000..13e65b37d --- /dev/null +++ b/rsocket-examples/src/test/resources/logback-test.xml @@ -0,0 +1,33 @@ + + + + + + + + %d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] %c{1} - %m%n + + + + + + + + + + + diff --git a/rsocket-load-balancer/build.gradle b/rsocket-load-balancer/build.gradle index a2c8b73c7..748f95de6 100644 --- a/rsocket-load-balancer/build.gradle +++ b/rsocket-load-balancer/build.gradle @@ -34,6 +34,7 @@ dependencies { testCompileOnly 'junit:junit' testImplementation 'org.hamcrest:hamcrest-library' testRuntimeOnly 'org.junit.vintage:junit-vintage-engine' + testRuntimeOnly 'ch.qos.logback:logback-classic' } description = 'Transparent Load Balancer for RSocket' diff --git a/rsocket-load-balancer/src/test/resources/log4j.properties b/rsocket-load-balancer/src/test/resources/log4j.properties deleted file mode 100644 index 8fc3a9cdd..000000000 --- a/rsocket-load-balancer/src/test/resources/log4j.properties +++ /dev/null @@ -1,20 +0,0 @@ -# -# Copyright 2015-2018 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. -# -log4j.rootLogger=INFO, stdout - -log4j.appender.stdout=org.apache.log4j.ConsoleAppender -log4j.appender.stdout.layout=org.apache.log4j.PatternLayout -log4j.appender.stdout.layout.ConversionPattern=%d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] (%F:%L) - %m%n \ No newline at end of file diff --git a/rsocket-load-balancer/src/test/resources/logback-test.xml b/rsocket-load-balancer/src/test/resources/logback-test.xml new file mode 100644 index 000000000..13e65b37d --- /dev/null +++ b/rsocket-load-balancer/src/test/resources/logback-test.xml @@ -0,0 +1,33 @@ + + + + + + + + %d{dd MMM yyyy HH:mm:ss,SSS} %5p [%t] %c{1} - %m%n + + + + + + + + + + + From a81b6391275054f0cff33a46b432b9d34a90b5d2 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 27 Apr 2020 13:45:34 +0100 Subject: [PATCH 152/181] Default errorConsumer to no-op in the new API (#801) --- rsocket-core/src/main/java/io/rsocket/RSocketFactory.java | 6 ++++-- .../src/main/java/io/rsocket/core/RSocketConnector.java | 2 +- .../src/main/java/io/rsocket/core/RSocketServer.java | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index f36a84241..178cc4fa9 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -111,7 +111,7 @@ public static class ClientRSocketFactory implements ClientTransportAcceptor { private Resume resume; public ClientRSocketFactory() { - this(RSocketConnector.create()); + this(RSocketConnector.create().errorConsumer(Throwable::printStackTrace)); } public ClientRSocketFactory(RSocketConnector connector) { @@ -393,6 +393,7 @@ public ClientRSocketFactory fragment(int mtu) { return this; } + /** @deprecated this is deprecated with no replacement. */ public ClientRSocketFactory errorConsumer(Consumer errorConsumer) { connector.errorConsumer(errorConsumer); return this; @@ -416,7 +417,7 @@ public static class ServerRSocketFactory implements ServerTransportAcceptor { private Resume resume; public ServerRSocketFactory() { - this(RSocketServer.create()); + this(RSocketServer.create().errorConsumer(Throwable::printStackTrace)); } public ServerRSocketFactory(RSocketServer server) { @@ -496,6 +497,7 @@ public ServerRSocketFactory fragment(int mtu) { return this; } + /** @deprecated this is deprecated with no replacement. */ public ServerRSocketFactory errorConsumer(Consumer errorConsumer) { server.errorConsumer(errorConsumer); return this; diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index ed20f892e..57aebbdf0 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -69,7 +69,7 @@ public class RSocketConnector { private int mtu = 0; private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; - private Consumer errorConsumer = Throwable::printStackTrace; + private Consumer errorConsumer = ex -> {}; private RSocketConnector() {} diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index 6c289d707..c82a2f40a 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -52,7 +52,7 @@ public final class RSocketServer { private Resume resume; private Supplier> leasesSupplier = null; - private Consumer errorConsumer = Throwable::printStackTrace; + private Consumer errorConsumer = ex -> {}; private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; private RSocketServer() {} From ad12dddc75760bb98f1107a810b997ee611e6455 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 27 Apr 2020 16:45:21 +0300 Subject: [PATCH 153/181] forces TcpDuplexConnection to use connection allocator Signed-off-by: Oleh Dokuka --- .../java/io/rsocket/transport/netty/TcpDuplexConnection.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java index aa28ab8b9..d71d6b356 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java @@ -31,7 +31,6 @@ public final class TcpDuplexConnection extends BaseDuplexConnection { private final Connection connection; - private final ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; private final boolean encodeLength; /** @@ -89,7 +88,7 @@ public Mono send(Publisher frames) { private ByteBuf encode(ByteBuf frame) { if (encodeLength) { - return FrameLengthFlyweight.encode(allocator, frame.readableBytes(), frame); + return FrameLengthFlyweight.encode(alloc(), frame.readableBytes(), frame); } else { return frame; } From 1c3927b2fd271dd690b7a01360db0414280639de Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 27 Apr 2020 21:27:42 +0300 Subject: [PATCH 154/181] updates to the latest reactor release-train Signed-off-by: Oleh Dokuka --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index dd5135f01..2c7757e0f 100644 --- a/build.gradle +++ b/build.gradle @@ -27,7 +27,7 @@ subprojects { apply plugin: 'io.spring.dependency-management' apply plugin: 'com.github.sherter.google-java-format' - ext['reactor-bom.version'] = 'Dysprosium-SR6' + ext['reactor-bom.version'] = 'Dysprosium-SR7' ext['logback.version'] = '1.2.3' ext['findbugs.version'] = '3.0.2' ext['netty-bom.version'] = '4.1.48.Final' From a5706bf6b92605312862d9a915d3f2213ae59773 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 27 Apr 2020 23:26:36 +0300 Subject: [PATCH 155/181] provides more `ByteBuf` leaks fixes (#803) 1. Ensures if there goes something during `Payload` to the frame we will not get any memory leaks in the end 2. Fixes `onDiscard` leak which did not work correctly because `actual` subscriber was nulled to early 3. Ensures that we will not have the wrong frame order in case of racing the first `Payload` sending and cancel. Also, it moves `onDiscard` hook to the very bottom in case of `requestChannel` to ensure the first payload is not leaked 4. Enables a set of tests that ensures that #757 and #733 are fully or partially fixed. --- .../io/rsocket/core/RSocketRequester.java | 162 ++++++++++++------ .../frame/DataAndMetadataFlyweight.java | 30 +++- .../io/rsocket/frame/LeaseFrameFlyweight.java | 16 +- .../frame/MetadataPushFrameFlyweight.java | 10 +- .../rsocket/frame/PayloadFrameFlyweight.java | 29 +++- .../frame/RequestChannelFrameFlyweight.java | 29 +++- .../RequestFireAndForgetFrameFlyweight.java | 29 +++- .../frame/RequestResponseFrameFlyweight.java | 29 +++- .../frame/RequestStreamFrameFlyweight.java | 29 +++- .../internal/UnicastMonoProcessor.java | 16 +- .../buffer/LeaksTrackingByteBufAllocator.java | 18 +- .../io/rsocket/core/RSocketRequesterTest.java | 143 +++++++++++----- .../io/rsocket/core/RSocketResponderTest.java | 69 ++++---- .../rsocket/frame/ByteBufRepresentation.java | 8 +- 14 files changed, 437 insertions(+), 180 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 21112c24d..fefb06003 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -54,7 +54,7 @@ import io.rsocket.util.MonoLifecycleHandler; import java.nio.channels.ClosedChannelException; import java.util.concurrent.CancellationException; -import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; import java.util.function.LongConsumer; @@ -260,6 +260,7 @@ public void doOnTerminal( removeStreamReceiver(streamId); } }); + receivers.put(streamId, receiver); return receiver.doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); @@ -281,7 +282,7 @@ private Flux handleRequestStream(final Payload payload) { final UnboundedProcessor sendProcessor = this.sendProcessor; final UnicastProcessor receiver = UnicastProcessor.create(); - final AtomicBoolean payloadReleasedFlag = new AtomicBoolean(false); + final AtomicInteger wip = new AtomicInteger(0); receivers.put(streamId, receiver); @@ -293,30 +294,49 @@ private Flux handleRequestStream(final Payload payload) { @Override public void accept(long n) { - if (firstRequest && !receiver.isDisposed()) { + if (firstRequest) { firstRequest = false; - if (!payloadReleasedFlag.getAndSet(true)) { - sendProcessor.onNext( - RequestStreamFrameFlyweight.encodeReleasingPayload( - allocator, streamId, n, payload)); + if (wip.getAndIncrement() != 0) { + // no need to do anything. + // stream was canceled and fist payload has already been discarded + return; } - } else if (contains(streamId) && !receiver.isDisposed()) { + int missed = 1; + boolean firstHasBeenSent = false; + for (; ; ) { + if (!firstHasBeenSent) { + sendProcessor.onNext( + RequestStreamFrameFlyweight.encodeReleasingPayload( + allocator, streamId, n, payload)); + firstHasBeenSent = true; + } else { + // if first frame was sent but we cycling again, it means that wip was + // incremented at doOnCancel + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + return; + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + return; + } + } + } else if (!receiver.isDisposed()) { sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); } } }) - .doOnError( - t -> { - if (contains(streamId) && !receiver.isDisposed()) { - sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t)); - } - }) .doOnCancel( () -> { - if (!payloadReleasedFlag.getAndSet(true)) { - payload.release(); + if (wip.getAndIncrement() != 0) { + return; } - if (contains(streamId) && !receiver.isDisposed()) { + + // check if we need to release payload + // only applicable if the cancel appears earlier than actual request + if (payload.refCnt() > 0) { + payload.release(); + } else { sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); } }) @@ -330,30 +350,32 @@ private Flux handleChannel(Flux request) { return Flux.error(err); } - return request.switchOnFirst( - (s, flux) -> { - Payload payload = s.get(); - if (payload != null) { - if (!PayloadValidationUtils.isValid(mtu, payload)) { - payload.release(); - final IllegalArgumentException t = - new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); - errorConsumer.accept(t); - return Mono.error(t); - } - return handleChannel(payload, flux); - } else { - return flux; - } - }, - false); + return request + .switchOnFirst( + (s, flux) -> { + Payload payload = s.get(); + if (payload != null) { + if (!PayloadValidationUtils.isValid(mtu, payload)) { + payload.release(); + final IllegalArgumentException t = + new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); + errorConsumer.accept(t); + return Mono.error(t); + } + return handleChannel(payload, flux); + } else { + return flux; + } + }, + false) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); } private Flux handleChannel(Payload initialPayload, Flux inboundFlux) { final UnboundedProcessor sendProcessor = this.sendProcessor; - final AtomicBoolean payloadReleasedFlag = new AtomicBoolean(false); final int streamId = streamIdSupplier.nextStreamId(receivers); + final AtomicInteger wip = new AtomicInteger(0); final UnicastProcessor receiver = UnicastProcessor.create(); final BaseSubscriber upstreamSubscriber = new BaseSubscriber() { @@ -421,19 +443,47 @@ protected void hookFinally(SignalType type) { public void accept(long n) { if (firstRequest) { firstRequest = false; - senders.put(streamId, upstreamSubscriber); - receivers.put(streamId, receiver); - - inboundFlux - .limitRate(Queues.SMALL_BUFFER_SIZE) - .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER) - .subscribe(upstreamSubscriber); - if (!payloadReleasedFlag.getAndSet(true)) { - ByteBuf frame = - RequestChannelFrameFlyweight.encodeReleasingPayload( - allocator, streamId, false, n, initialPayload); - - sendProcessor.onNext(frame); + if (wip.getAndIncrement() != 0) { + // no need to do anything. + // stream was canceled and fist payload has already been discarded + return; + } + int missed = 1; + boolean firstHasBeenSent = false; + for (; ; ) { + if (!firstHasBeenSent) { + ByteBuf frame; + try { + frame = + RequestChannelFrameFlyweight.encodeReleasingPayload( + allocator, streamId, false, n, initialPayload); + } catch (IllegalReferenceCountException e) { + return; + } + + senders.put(streamId, upstreamSubscriber); + receivers.put(streamId, receiver); + + inboundFlux + .limitRate(Queues.SMALL_BUFFER_SIZE) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER) + .subscribe(upstreamSubscriber); + + sendProcessor.onNext(frame); + firstHasBeenSent = true; + } else { + // if first frame was sent but we cycling again, it means that wip was + // incremented at doOnCancel + senders.remove(streamId, upstreamSubscriber); + receivers.remove(streamId, receiver); + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + return; + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + return; + } } } else { sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); @@ -442,22 +492,22 @@ public void accept(long n) { }) .doOnError( t -> { - if (receivers.remove(streamId, receiver)) { - upstreamSubscriber.cancel(); - } + upstreamSubscriber.cancel(); + receivers.remove(streamId, receiver); }) .doOnComplete(() -> receivers.remove(streamId, receiver)) .doOnCancel( () -> { - if (!payloadReleasedFlag.getAndSet(true)) { - initialPayload.release(); + upstreamSubscriber.cancel(); + if (wip.getAndIncrement() != 0) { + return; } + + // need to send frame only if RequestChannelFrame was sent if (receivers.remove(streamId, receiver)) { sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - upstreamSubscriber.cancel(); } - }) - .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); + }); } private Mono handleMetadataPush(Payload payload) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java index ea54aa374..73bfd38f1 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java @@ -37,8 +37,34 @@ static ByteBuf encode( boolean hasMetadata, ByteBuf data) { - final boolean addData = data != null && data.isReadable(); - final boolean addMetadata = hasMetadata && metadata.isReadable(); + final boolean addData; + if (data != null) { + if (data.isReadable()) { + addData = true; + } else { + // even though there is nothing to read, we still have to release here since nobody else + // going to do soo + data.release(); + addData = false; + } + } else { + addData = false; + } + + final boolean addMetadata; + if (hasMetadata) { + if (metadata.isReadable()) { + addMetadata = true; + } else { + // even though there is nothing to read, we still have to release here since nobody else + // going to do soo + metadata.release(); + addMetadata = false; + } + } else { + // has no metadata means it is null, thus no need to release anything + addMetadata = false; + } if (hasMetadata) { int length = metadata.readableBytes(); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java index 039c72886..32f086a15 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java @@ -14,7 +14,6 @@ public static ByteBuf encode( @Nullable final ByteBuf metadata) { final boolean hasMetadata = metadata != null; - final boolean addMetadata = hasMetadata && metadata.isReadable(); int flags = 0; @@ -27,6 +26,21 @@ public static ByteBuf encode( .writeInt(ttl) .writeInt(numRequests); + final boolean addMetadata; + if (hasMetadata) { + if (metadata.isReadable()) { + addMetadata = true; + } else { + // even though there is nothing to read, we still have to release here since nobody else + // going to do soo + metadata.release(); + addMetadata = false; + } + } else { + // has no metadata means it is null, thus no need to release anything + addMetadata = false; + } + if (addMetadata) { return allocator.compositeBuffer(2).addComponents(true, header, metadata); } else { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameFlyweight.java index e3a9a47ba..a39acef92 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameFlyweight.java @@ -2,13 +2,21 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; import io.rsocket.Payload; public class MetadataPushFrameFlyweight { public static ByteBuf encodeReleasingPayload(ByteBufAllocator allocator, Payload payload) { final ByteBuf metadata = payload.metadata().retain(); - payload.release(); + // releasing payload safely since it can be already released wheres we have to release retained + // data and metadata as well + try { + payload.release(); + } catch (IllegalReferenceCountException e) { + metadata.release(); + throw e; + } return encode(allocator, metadata); } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java index 4c2ebdf6e..53ac6150b 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java @@ -2,6 +2,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; import io.rsocket.Payload; public class PayloadFrameFlyweight { @@ -23,11 +24,31 @@ public static ByteBuf encodeNextCompleteReleasingPayload( static ByteBuf encodeReleasingPayload( ByteBufAllocator allocator, int streamId, boolean complete, Payload payload) { - final boolean hasMetadata = payload.hasMetadata(); + // if refCnt exceptions throws here it is safe to do no-op + boolean hasMetadata = payload.hasMetadata(); + // if refCnt exceptions throws here it is safe to do no-op still final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; - final ByteBuf data = payload.data().retain(); - - payload.release(); + final ByteBuf data; + // retaining data safely. May throw either NPE or RefCntE + try { + data = payload.data().retain(); + } catch (IllegalReferenceCountException | NullPointerException e) { + if (hasMetadata) { + metadata.release(); + } + throw e; + } + // releasing payload safely since it can be already released wheres we have to release retained + // data and metadata as well + try { + payload.release(); + } catch (IllegalReferenceCountException e) { + data.release(); + if (hasMetadata) { + metadata.release(); + } + throw e; + } return encode(allocator, streamId, false, complete, true, metadata, data); } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java index 7c3cbb574..c0db21170 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java @@ -2,6 +2,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; import io.rsocket.Payload; public class RequestChannelFrameFlyweight { @@ -17,11 +18,31 @@ public static ByteBuf encodeReleasingPayload( long initialRequestN, Payload payload) { - final boolean hasMetadata = payload.hasMetadata(); + // if refCnt exceptions throws here it is safe to do no-op + boolean hasMetadata = payload.hasMetadata(); + // if refCnt exceptions throws here it is safe to do no-op still final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; - final ByteBuf data = payload.data().retain(); - - payload.release(); + final ByteBuf data; + // retaining data safely. May throw either NPE or RefCntE + try { + data = payload.data().retain(); + } catch (IllegalReferenceCountException | NullPointerException e) { + if (hasMetadata) { + metadata.release(); + } + throw e; + } + // releasing payload safely since it can be already released wheres we have to release retained + // data and metadata as well + try { + payload.release(); + } catch (IllegalReferenceCountException e) { + data.release(); + if (hasMetadata) { + metadata.release(); + } + throw e; + } return encode(allocator, streamId, false, complete, initialRequestN, metadata, data); } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java index 287f765f7..e091edcc3 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java @@ -2,6 +2,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; import io.rsocket.Payload; public class RequestFireAndForgetFrameFlyweight { @@ -13,11 +14,31 @@ private RequestFireAndForgetFrameFlyweight() {} public static ByteBuf encodeReleasingPayload( ByteBufAllocator allocator, int streamId, Payload payload) { - final boolean hasMetadata = payload.hasMetadata(); + // if refCnt exceptions throws here it is safe to do no-op + boolean hasMetadata = payload.hasMetadata(); + // if refCnt exceptions throws here it is safe to do no-op still final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; - final ByteBuf data = payload.data().retain(); - - payload.release(); + final ByteBuf data; + // retaining data safely. May throw either NPE or RefCntE + try { + data = payload.data().retain(); + } catch (IllegalReferenceCountException | NullPointerException e) { + if (hasMetadata) { + metadata.release(); + } + throw e; + } + // releasing payload safely since it can be already released wheres we have to release retained + // data and metadata as well + try { + payload.release(); + } catch (IllegalReferenceCountException e) { + data.release(); + if (hasMetadata) { + metadata.release(); + } + throw e; + } return FLYWEIGHT.encode(allocator, streamId, false, metadata, data); } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java index 3fbac27d2..782c70965 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java @@ -2,6 +2,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; import io.rsocket.Payload; public class RequestResponseFrameFlyweight { @@ -13,11 +14,31 @@ private RequestResponseFrameFlyweight() {} public static ByteBuf encodeReleasingPayload( ByteBufAllocator allocator, int streamId, Payload payload) { - final boolean hasMetadata = payload.hasMetadata(); + // if refCnt exceptions throws here it is safe to do no-op + boolean hasMetadata = payload.hasMetadata(); + // if refCnt exceptions throws here it is safe to do no-op still final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; - final ByteBuf data = payload.data().retain(); - - payload.release(); + final ByteBuf data; + // retaining data safely. May throw either NPE or RefCntE + try { + data = payload.data().retain(); + } catch (IllegalReferenceCountException | NullPointerException e) { + if (hasMetadata) { + metadata.release(); + } + throw e; + } + // releasing payload safely since it can be already released wheres we have to release retained + // data and metadata as well + try { + payload.release(); + } catch (IllegalReferenceCountException e) { + data.release(); + if (hasMetadata) { + metadata.release(); + } + throw e; + } return encode(allocator, streamId, false, metadata, data); } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java index ff1435652..2fb209ffb 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java @@ -2,6 +2,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; import io.rsocket.Payload; public class RequestStreamFrameFlyweight { @@ -13,11 +14,31 @@ private RequestStreamFrameFlyweight() {} public static ByteBuf encodeReleasingPayload( ByteBufAllocator allocator, int streamId, long initialRequestN, Payload payload) { - final boolean hasMetadata = payload.hasMetadata(); + // if refCnt exceptions throws here it is safe to do no-op + boolean hasMetadata = payload.hasMetadata(); + // if refCnt exceptions throws here it is safe to do no-op still final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; - final ByteBuf data = payload.data().retain(); - - payload.release(); + final ByteBuf data; + // retaining data safely. May throw either NPE or RefCntE + try { + data = payload.data().retain(); + } catch (IllegalReferenceCountException | NullPointerException e) { + if (hasMetadata) { + metadata.release(); + } + throw e; + } + // releasing payload safely since it can be already released wheres we have to release retained + // data and metadata as well + try { + payload.release(); + } catch (IllegalReferenceCountException e) { + data.release(); + if (hasMetadata) { + metadata.release(); + } + throw e; + } return encode(allocator, streamId, false, initialRequestN, metadata, data); } diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java index af4c8768b..c5b06e086 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java @@ -99,6 +99,7 @@ public static UnicastMonoProcessor create(MonoLifecycleHandler lifecyc UnicastMonoProcessor.class, Subscription.class, "subscription"); CoreSubscriber actual; + boolean hasDownstream = false; Throwable error; O value; @@ -185,7 +186,7 @@ private void complete(O v) { if (state == HAS_REQUEST_NO_RESULT) { if (STATE.compareAndSet(this, HAS_REQUEST_NO_RESULT, HAS_REQUEST_HAS_RESULT)) { final Subscriber a = actual; - actual = null; + hasDownstream = false; value = null; lifecycleHandler.doOnTerminal(SignalType.ON_COMPLETE, v, null); a.onNext(v); @@ -222,7 +223,7 @@ private void complete() { if (state == HAS_REQUEST_NO_RESULT || state == NO_REQUEST_NO_RESULT) { if (STATE.compareAndSet(this, state, HAS_REQUEST_HAS_RESULT)) { final Subscriber a = actual; - actual = null; + hasDownstream = false; lifecycleHandler.doOnTerminal(SignalType.ON_COMPLETE, null, null); a.onComplete(); return; @@ -256,7 +257,7 @@ private void complete(Throwable e) { if (state == HAS_REQUEST_NO_RESULT || state == NO_REQUEST_NO_RESULT) { if (STATE.compareAndSet(this, state, HAS_REQUEST_HAS_RESULT)) { final Subscriber a = actual; - actual = null; + hasDownstream = false; lifecycleHandler.doOnTerminal(SignalType.ON_ERROR, null, e); a.onError(e); return; @@ -278,6 +279,7 @@ public void subscribe(CoreSubscriber actual) { lh.doOnSubscribe(); + this.hasDownstream = true; this.actual = actual; int state = this.state; @@ -303,7 +305,7 @@ public void subscribe(CoreSubscriber actual) { // no value // e.g. [onError / onComplete / dispose] only if (state == NO_REQUEST_HAS_RESULT && this.value == null) { - this.actual = null; + this.hasDownstream = false; Throwable e = this.error; // barrier to flush changes STATE.set(this, HAS_REQUEST_HAS_RESULT); @@ -340,7 +342,7 @@ public final void request(long n) { if (STATE.compareAndSet(this, NO_REQUEST_HAS_RESULT, HAS_REQUEST_HAS_RESULT)) { final Subscriber a = actual; final O v = value; - actual = null; + hasDownstream = false; value = null; lifecycleHandler.doOnTerminal(SignalType.ON_COMPLETE, v, null); a.onNext(v); @@ -360,7 +362,7 @@ public final void cancel() { if (STATE.getAndSet(this, CANCELLED) <= HAS_REQUEST_NO_RESULT) { Operators.onDiscard(value, currentContext()); value = null; - actual = null; + hasDownstream = false; lifecycleHandler.doOnTerminal(SignalType.CANCEL, null, null); final Subscription s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription()); if (s != null && s != Operators.cancelledSubscription()) { @@ -502,6 +504,6 @@ public Object scanUnsafe(Attr key) { * @return true if any {@link Subscriber} is actively subscribed */ public final boolean hasDownstream() { - return state > NO_SUBSCRIBER_HAS_RESULT && actual != null; + return state > NO_SUBSCRIBER_HAS_RESULT && hasDownstream; } } diff --git a/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java b/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java index 2044779ef..800e5d678 100644 --- a/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java +++ b/rsocket-core/src/test/java/io/rsocket/buffer/LeaksTrackingByteBufAllocator.java @@ -3,7 +3,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; -import java.util.List; import java.util.concurrent.ConcurrentLinkedQueue; import org.assertj.core.api.Assertions; @@ -35,22 +34,9 @@ public LeaksTrackingByteBufAllocator assertHasNoLeaks() { try { Assertions.assertThat(tracker) .allSatisfy( - buf -> { - if (buf instanceof CompositeByteBuf) { - if (buf.refCnt() > 0) { - List decomposed = - ((CompositeByteBuf) buf).decompose(0, buf.readableBytes()); - for (int i = 0; i < decomposed.size(); i++) { - Assertions.assertThat(decomposed.get(i)) - .matches(bb -> bb.refCnt() == 0, "Got unreleased CompositeByteBuf"); - } - } - - } else { + buf -> Assertions.assertThat(buf) - .matches(bb -> bb.refCnt() == 0, "buffer should be released"); - } - }); + .matches(bb -> bb.refCnt() == 0, "buffer should be released")); } finally { tracker.clear(); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index e536d2db4..3b62bc437 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -38,6 +38,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; +import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; import io.rsocket.Payload; import io.rsocket.RSocket; @@ -71,6 +72,7 @@ import java.util.function.Function; import java.util.stream.Stream; import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -84,6 +86,7 @@ import org.reactivestreams.Subscription; import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; +import reactor.core.publisher.Hooks; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; import reactor.core.publisher.UnicastProcessor; @@ -97,6 +100,8 @@ public class RSocketRequesterTest { @BeforeEach public void setUp() throws Throwable { + Hooks.onNextDropped(ReferenceCountUtil::safeRelease); + Hooks.onErrorDropped((t) -> {}); rule = new ClientSocketRule(); rule.apply( new Statement() { @@ -107,6 +112,12 @@ public void evaluate() {} .evaluate(); } + @AfterEach + public void tearDown() { + Hooks.resetOnErrorDropped(); + Hooks.resetOnNextDropped(); + } + @Test @Timeout(2_000) public void testInvalidFrameOnStream0() { @@ -403,21 +414,8 @@ static Stream>> prepareCalls() { rule.assertHasNoLeaks(); } - @Test - @Disabled("Due to https://github.com/reactor/reactor-core/pull/2114") - @SuppressWarnings("unchecked") - public void checkNoLeaksOnRacingTest() { - - racingCases() - .forEach( - a -> { - ((Runnable) a.get()[0]).run(); - checkNoLeaksOnRacing( - (Function>) a.get()[1], - (BiConsumer, ClientSocketRule>) a.get()[2]); - }); - } - + @ParameterizedTest + @MethodSource("racingCases") public void checkNoLeaksOnRacing( Function> initiator, BiConsumer, ClientSocketRule> runner) { @@ -437,7 +435,7 @@ public void evaluate() {} } Publisher payloadP = initiator.apply(clientSocketRule); - AssertSubscriber assertSubscriber = AssertSubscriber.create(); + AssertSubscriber assertSubscriber = AssertSubscriber.create(0); if (payloadP instanceof Flux) { ((Flux) payloadP).doOnNext(Payload::release).subscribe(assertSubscriber); @@ -450,14 +448,13 @@ public void evaluate() {} Assertions.assertThat(clientSocketRule.connection.getSent()) .allMatch(ReferenceCounted::release); - rule.assertHasNoLeaks(); + clientSocketRule.assertHasNoLeaks(); } } private static Stream racingCases() { return Stream.of( Arguments.of( - (Runnable) () -> System.out.println("RequestStream downstream cancellation case"), (Function>) (rule) -> rule.socket.requestStream(EmptyPayload.INSTANCE), (BiConsumer, ClientSocketRule>) @@ -467,6 +464,7 @@ private static Stream racingCases() { metadata.writeCharSequence("abc", CharsetUtil.UTF_8); ByteBuf data = allocator.buffer(); data.writeCharSequence("def", CharsetUtil.UTF_8); + as.request(1); int streamId = rule.getStreamIdForRequestType(REQUEST_STREAM); ByteBuf frame = PayloadFrameFlyweight.encode( @@ -475,7 +473,6 @@ private static Stream racingCases() { RaceTestUtils.race(as::cancel, () -> rule.connection.addToReceivedBuffer(frame)); }), Arguments.of( - (Runnable) () -> System.out.println("RequestChannel downstream cancellation case"), (Function>) (rule) -> rule.socket.requestChannel(Flux.just(EmptyPayload.INSTANCE)), (BiConsumer, ClientSocketRule>) @@ -485,6 +482,7 @@ private static Stream racingCases() { metadata.writeCharSequence("abc", CharsetUtil.UTF_8); ByteBuf data = allocator.buffer(); data.writeCharSequence("def", CharsetUtil.UTF_8); + as.request(1); int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); ByteBuf frame = PayloadFrameFlyweight.encode( @@ -493,79 +491,143 @@ private static Stream racingCases() { RaceTestUtils.race(as::cancel, () -> rule.connection.addToReceivedBuffer(frame)); }), Arguments.of( - (Runnable) () -> System.out.println("RequestChannel upstream cancellation 1"), (Function>) (rule) -> { ByteBufAllocator allocator = rule.alloc(); ByteBuf metadata = allocator.buffer(); - metadata.writeCharSequence("abc", CharsetUtil.UTF_8); + metadata.writeCharSequence("metadata", CharsetUtil.UTF_8); ByteBuf data = allocator.buffer(); - data.writeCharSequence("def", CharsetUtil.UTF_8); - return rule.socket.requestChannel( - Flux.just(ByteBufPayload.create(data, metadata))); + data.writeCharSequence("data", CharsetUtil.UTF_8); + final Payload payload = ByteBufPayload.create(data, metadata); + + return rule.socket.requestStream(payload); }, (BiConsumer, ClientSocketRule>) (as, rule) -> { + RaceTestUtils.race(() -> as.request(1), as::cancel); + // ensures proper frames order + if (rule.connection.getSent().size() > 0) { + Assertions.assertThat(rule.connection.getSent()).hasSize(2); + Assertions.assertThat(rule.connection.getSent()) + .element(0) + .matches( + bb -> frameType(bb) == REQUEST_STREAM, + "Expected first frame matches {" + + REQUEST_STREAM + + "} but was {" + + frameType(rule.connection.getSent().stream().findFirst().get()) + + "}"); + Assertions.assertThat(rule.connection.getSent()) + .element(1) + .matches( + bb -> frameType(bb) == CANCEL, + "Expected first frame matches {" + + CANCEL + + "} but was {" + + frameType( + rule.connection.getSent().stream().skip(1).findFirst().get()) + + "}"); + } + }), + Arguments.of( + (Function>) + (rule) -> { ByteBufAllocator allocator = rule.alloc(); - int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); - ByteBuf frame = CancelFrameFlyweight.encode(allocator, streamId); - - RaceTestUtils.race( - () -> as.request(1), () -> rule.connection.addToReceivedBuffer(frame)); + return rule.socket.requestChannel( + Flux.generate( + () -> 1L, + (index, sink) -> { + ByteBuf metadata = allocator.buffer(); + metadata.writeCharSequence("metadata", CharsetUtil.UTF_8); + ByteBuf data = allocator.buffer(); + data.writeCharSequence("data", CharsetUtil.UTF_8); + final Payload payload = ByteBufPayload.create(data, metadata); + sink.next(payload); + sink.complete(); + return ++index; + })); + }, + (BiConsumer, ClientSocketRule>) + (as, rule) -> { + RaceTestUtils.race(() -> as.request(1), as::cancel); + // ensures proper frames order + if (rule.connection.getSent().size() > 0) { + Assertions.assertThat(rule.connection.getSent()).hasSize(2); + Assertions.assertThat(rule.connection.getSent()) + .element(0) + .matches( + bb -> frameType(bb) == REQUEST_CHANNEL, + "Expected first frame matches {" + + REQUEST_CHANNEL + + "} but was {" + + frameType(rule.connection.getSent().stream().findFirst().get()) + + "}"); + Assertions.assertThat(rule.connection.getSent()) + .element(1) + .matches( + bb -> frameType(bb) == CANCEL, + "Expected first frame matches {" + + CANCEL + + "} but was {" + + frameType( + rule.connection.getSent().stream().skip(1).findFirst().get()) + + "}"); + } }), Arguments.of( - (Runnable) () -> System.out.println("RequestChannel upstream cancellation 2"), (Function>) (rule) -> rule.socket.requestChannel( Flux.generate( () -> 1L, (index, sink) -> { - final Payload payload = - ByteBufPayload.create("d" + index, "m" + index); + ByteBuf data = rule.alloc().buffer(); + data.writeCharSequence("d" + index, CharsetUtil.UTF_8); + ByteBuf metadata = rule.alloc().buffer(); + metadata.writeCharSequence("m" + index, CharsetUtil.UTF_8); + final Payload payload = ByteBufPayload.create(data, metadata); sink.next(payload); return ++index; })), (BiConsumer, ClientSocketRule>) (as, rule) -> { ByteBufAllocator allocator = rule.alloc(); + as.request(1); int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); ByteBuf frame = CancelFrameFlyweight.encode(allocator, streamId); - as.request(1); - RaceTestUtils.race( () -> as.request(Long.MAX_VALUE), () -> rule.connection.addToReceivedBuffer(frame)); }), Arguments.of( - (Runnable) () -> System.out.println("RequestChannel remote error"), (Function>) (rule) -> rule.socket.requestChannel( Flux.generate( () -> 1L, (index, sink) -> { - final Payload payload = - ByteBufPayload.create("d" + index, "m" + index); + ByteBuf data = rule.alloc().buffer(); + data.writeCharSequence("d" + index, CharsetUtil.UTF_8); + ByteBuf metadata = rule.alloc().buffer(); + metadata.writeCharSequence("m" + index, CharsetUtil.UTF_8); + final Payload payload = ByteBufPayload.create(data, metadata); sink.next(payload); return ++index; })), (BiConsumer, ClientSocketRule>) (as, rule) -> { ByteBufAllocator allocator = rule.alloc(); + as.request(1); int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); ByteBuf frame = ErrorFrameFlyweight.encode(allocator, streamId, new RuntimeException("test")); - as.request(1); - RaceTestUtils.race( () -> as.request(Long.MAX_VALUE), () -> rule.connection.addToReceivedBuffer(frame)); }), Arguments.of( - (Runnable) () -> System.out.println("RequestResponse downstream cancellation"), (Function>) (rule) -> rule.socket.requestResponse(EmptyPayload.INSTANCE), (BiConsumer, ClientSocketRule>) @@ -575,6 +637,7 @@ private static Stream racingCases() { metadata.writeCharSequence("abc", CharsetUtil.UTF_8); ByteBuf data = allocator.buffer(); data.writeCharSequence("def", CharsetUtil.UTF_8); + as.request(Long.MAX_VALUE); int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); ByteBuf frame = PayloadFrameFlyweight.encode( diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index 48910b3a2..c19456548 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -94,6 +94,8 @@ public class RSocketResponderTest { @BeforeEach public void setUp() throws Throwable { + Hooks.onNextDropped(ReferenceCountUtil::safeRelease); + Hooks.onErrorDropped(t -> {}); rule = new ServerSocketRule(); rule.apply( new Statement() { @@ -107,6 +109,7 @@ public void evaluate() {} @AfterEach public void tearDown() { Hooks.resetOnErrorDropped(); + Hooks.resetOnNextDropped(); } @Test @@ -247,9 +250,7 @@ protected void hookOnSubscribe(Subscription subscription) { } @Test - @Disabled("Due to https://github.com/reactor/reactor-core/pull/2114") public void checkNoLeaksOnRacingCancelFromRequestChannelAndNextFromUpstream() { - ByteBufAllocator allocator = rule.alloc(); for (int i = 0; i < 10000; i++) { AssertSubscriber assertSubscriber = AssertSubscriber.create(); @@ -258,33 +259,32 @@ public void checkNoLeaksOnRacingCancelFromRequestChannelAndNextFromUpstream() { new AbstractRSocket() { @Override public Flux requestChannel(Publisher payloads) { - ((Flux) payloads) - .doOnNext(ReferenceCountUtil::safeRelease) - .subscribe(assertSubscriber); + payloads.subscribe(assertSubscriber); return Flux.never(); } }, Integer.MAX_VALUE); rule.sendRequest(1, REQUEST_CHANNEL); + ByteBuf metadata1 = allocator.buffer(); - metadata1.writeCharSequence("abc", CharsetUtil.UTF_8); + metadata1.writeCharSequence("abc1", CharsetUtil.UTF_8); ByteBuf data1 = allocator.buffer(); - data1.writeCharSequence("def", CharsetUtil.UTF_8); + data1.writeCharSequence("def1", CharsetUtil.UTF_8); ByteBuf nextFrame1 = PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata1, data1); ByteBuf metadata2 = allocator.buffer(); - metadata2.writeCharSequence("abc", CharsetUtil.UTF_8); + metadata2.writeCharSequence("abc2", CharsetUtil.UTF_8); ByteBuf data2 = allocator.buffer(); - data2.writeCharSequence("def", CharsetUtil.UTF_8); + data2.writeCharSequence("def2", CharsetUtil.UTF_8); ByteBuf nextFrame2 = PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata2, data2); ByteBuf metadata3 = allocator.buffer(); - metadata3.writeCharSequence("abc", CharsetUtil.UTF_8); + metadata3.writeCharSequence("abc3", CharsetUtil.UTF_8); ByteBuf data3 = allocator.buffer(); - data3.writeCharSequence("def", CharsetUtil.UTF_8); + data3.writeCharSequence("def3", CharsetUtil.UTF_8); ByteBuf nextFrame3 = PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata3, data3); @@ -294,6 +294,8 @@ public Flux requestChannel(Publisher payloads) { }, assertSubscriber::cancel); + Assertions.assertThat(assertSubscriber.values()).allMatch(ReferenceCounted::release); + Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); @@ -301,7 +303,6 @@ public Flux requestChannel(Publisher payloads) { } @Test - @Disabled("Due to https://github.com/reactor/reactor-core/pull/2114") public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChannelTest() { Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); @@ -341,7 +342,6 @@ public Flux requestChannel(Publisher payloads) { } @Test - @Disabled("Due to https://github.com/reactor/reactor-core/pull/2114") public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChannelTest1() { Scheduler parallel = Schedulers.parallel(); Hooks.onErrorDropped((e) -> {}); @@ -388,27 +388,25 @@ public Flux requestChannel(Publisher payloads) { } @Test - @Disabled("Due to https://github.com/reactor/reactor-core/pull/2114") public void - checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromUpstreamOnErrorFromRequestChannelTest1() - throws InterruptedException { + checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromUpstreamOnErrorFromRequestChannelTest1() { Scheduler parallel = Schedulers.parallel(); Hooks.onErrorDropped((e) -> {}); ByteBufAllocator allocator = rule.alloc(); for (int i = 0; i < 10000; i++) { FluxSink[] sinks = new FluxSink[1]; - + AssertSubscriber assertSubscriber = AssertSubscriber.create(); rule.setAcceptingSocket( new AbstractRSocket() { @Override public Flux requestChannel(Publisher payloads) { + payloads.subscribe(assertSubscriber); return Flux.create( - sink -> { - sinks[0] = sink; - }, - FluxSink.OverflowStrategy.IGNORE) - .mergeWith(payloads); + sink -> { + sinks[0] = sink; + }, + FluxSink.OverflowStrategy.IGNORE); } }, 1); @@ -416,23 +414,23 @@ public Flux requestChannel(Publisher payloads) { rule.sendRequest(1, REQUEST_CHANNEL); ByteBuf metadata1 = allocator.buffer(); - metadata1.writeCharSequence("abc", CharsetUtil.UTF_8); + metadata1.writeCharSequence("abc1", CharsetUtil.UTF_8); ByteBuf data1 = allocator.buffer(); - data1.writeCharSequence("def", CharsetUtil.UTF_8); + data1.writeCharSequence("def1", CharsetUtil.UTF_8); ByteBuf nextFrame1 = PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata1, data1); ByteBuf metadata2 = allocator.buffer(); - metadata2.writeCharSequence("abc", CharsetUtil.UTF_8); + metadata2.writeCharSequence("abc2", CharsetUtil.UTF_8); ByteBuf data2 = allocator.buffer(); - data2.writeCharSequence("def", CharsetUtil.UTF_8); + data2.writeCharSequence("def2", CharsetUtil.UTF_8); ByteBuf nextFrame2 = PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata2, data2); ByteBuf metadata3 = allocator.buffer(); - metadata3.writeCharSequence("abc", CharsetUtil.UTF_8); + metadata3.writeCharSequence("abc3", CharsetUtil.UTF_8); ByteBuf data3 = allocator.buffer(); - data3.writeCharSequence("def", CharsetUtil.UTF_8); + data3.writeCharSequence("def3", CharsetUtil.UTF_8); ByteBuf nextFrame3 = PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata3, data3); @@ -454,13 +452,12 @@ public Flux requestChannel(Publisher payloads) { parallel); Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); - + Assertions.assertThat(assertSubscriber.values()).allMatch(ReferenceCounted::release); rule.assertHasNoLeaks(); } } @Test - @Disabled("Due to https://github.com/reactor/reactor-core/pull/2114") public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestStreamTest1() { Scheduler parallel = Schedulers.parallel(); Hooks.onErrorDropped((e) -> {}); @@ -585,23 +582,23 @@ public Flux requestChannel(Publisher payloads) { ByteBuf cancelFrame = CancelFrameFlyweight.encode(allocator, 1); ByteBuf metadata1 = allocator.buffer(); - metadata1.writeCharSequence("abc", CharsetUtil.UTF_8); + metadata1.writeCharSequence("abc1", CharsetUtil.UTF_8); ByteBuf data1 = allocator.buffer(); - data1.writeCharSequence("def", CharsetUtil.UTF_8); + data1.writeCharSequence("def1", CharsetUtil.UTF_8); ByteBuf nextFrame1 = PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata1, data1); ByteBuf metadata2 = allocator.buffer(); - metadata2.writeCharSequence("abc", CharsetUtil.UTF_8); + metadata2.writeCharSequence("abc2", CharsetUtil.UTF_8); ByteBuf data2 = allocator.buffer(); - data2.writeCharSequence("def", CharsetUtil.UTF_8); + data2.writeCharSequence("def2", CharsetUtil.UTF_8); ByteBuf nextFrame2 = PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata2, data2); ByteBuf metadata3 = allocator.buffer(); - metadata3.writeCharSequence("abc", CharsetUtil.UTF_8); + metadata3.writeCharSequence("abc3", CharsetUtil.UTF_8); ByteBuf data3 = allocator.buffer(); - data3.writeCharSequence("def", CharsetUtil.UTF_8); + data3.writeCharSequence("de3", CharsetUtil.UTF_8); ByteBuf nextFrame3 = PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata3, data3); rule.connection.addToReceivedBuffer(nextFrame1, nextFrame2, nextFrame3); diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java b/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java index 5e94935c5..63300c718 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/ByteBufRepresentation.java @@ -26,7 +26,13 @@ public final class ByteBufRepresentation extends StandardRepresentation { protected String fallbackToStringOf(Object object) { if (object instanceof ByteBuf) { try { - return ByteBufUtil.prettyHexDump((ByteBuf) object); + String normalBufferString = object.toString(); + String prettyHexDump = ByteBufUtil.prettyHexDump((ByteBuf) object); + return new StringBuilder() + .append(normalBufferString) + .append("\n") + .append(prettyHexDump) + .toString(); } catch (IllegalReferenceCountException e) { // noops } From 388e4e92e1f5028ff9cbff22f47b03abc9c91aec Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 27 Apr 2020 23:40:39 +0300 Subject: [PATCH 156/181] fixes readme and bumps versions Signed-off-by: Oleh Dokuka --- README.md | 8 ++++---- gradle.properties | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 538587154..0e055721e 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ repositories { mavenCentral() } dependencies { - implementation 'io.rsocket:rsocket-core:1.0.0-RC6' - implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC6' + implementation 'io.rsocket:rsocket-core:1.0.0-RC7' + implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC7' } ``` @@ -40,8 +40,8 @@ repositories { maven { url 'https://oss.jfrog.org/oss-snapshot-local' } } dependencies { - implementation 'io.rsocket:rsocket-core:1.0.0-RC7-SNAPSHOT' - implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC7-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:1.0.0-RC8-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC8-SNAPSHOT' } ``` diff --git a/gradle.properties b/gradle.properties index 3018f4792..2e429e05a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-RC7 -perfBaselineVersion=1.0.0-RC6 +version=1.0.0-RC8 +perfBaselineVersion=1.0.0-RC7 From 922aa20b2988b50211911037aa8a470dd07a33e3 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 28 Apr 2020 09:33:14 +0300 Subject: [PATCH 157/181] fixes compilation issues in perf tests Signed-off-by: Oleh Dokuka --- .../io/rsocket/{ => core}/RSocketPerf.java | 26 ++++++++++--------- .../{ => core}/StreamIdSupplierPerf.java | 12 +++++++-- 2 files changed, 24 insertions(+), 14 deletions(-) rename benchmarks/src/main/java/io/rsocket/{ => core}/RSocketPerf.java (90%) rename benchmarks/src/main/java/io/rsocket/{ => core}/StreamIdSupplierPerf.java (66%) diff --git a/benchmarks/src/main/java/io/rsocket/RSocketPerf.java b/benchmarks/src/main/java/io/rsocket/core/RSocketPerf.java similarity index 90% rename from benchmarks/src/main/java/io/rsocket/RSocketPerf.java rename to benchmarks/src/main/java/io/rsocket/core/RSocketPerf.java index 0c6515140..f78843f5b 100644 --- a/benchmarks/src/main/java/io/rsocket/RSocketPerf.java +++ b/benchmarks/src/main/java/io/rsocket/core/RSocketPerf.java @@ -1,5 +1,11 @@ -package io.rsocket; - +package io.rsocket.core; + +import io.rsocket.AbstractRSocket; +import io.rsocket.Closeable; +import io.rsocket.Payload; +import io.rsocket.PayloadsMaxPerfSubscriber; +import io.rsocket.PayloadsPerfSubscriber; +import io.rsocket.RSocket; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.transport.local.LocalClientTransport; import io.rsocket.transport.local.LocalServerTransport; @@ -59,9 +65,7 @@ public void awaitToBeConsumed() { @Setup public void setUp() throws NoSuchFieldException, IllegalAccessException { server = - RSocketFactory.receive() - .frameDecoder(PayloadDecoder.ZERO_COPY) - .acceptor( + RSocketServer.create( (setup, sendingSocket) -> Mono.just( new AbstractRSocket() { @@ -89,16 +93,14 @@ public Flux requestChannel(Publisher payloads) { return Flux.from(payloads); } })) - .transport(LocalServerTransport.create("server")) - .start() + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .bind(LocalServerTransport.create("server")) .block(); client = - RSocketFactory.connect() - .singleSubscriberRequester() - .frameDecoder(PayloadDecoder.ZERO_COPY) - .transport(LocalClientTransport.create("server")) - .start() + RSocketConnector.create() + .payloadDecoder(PayloadDecoder.ZERO_COPY) + .connect(LocalClientTransport.create("server")) .block(); Field sendProcessorField = RSocketRequester.class.getDeclaredField("sendProcessor"); diff --git a/benchmarks/src/main/java/io/rsocket/StreamIdSupplierPerf.java b/benchmarks/src/main/java/io/rsocket/core/StreamIdSupplierPerf.java similarity index 66% rename from benchmarks/src/main/java/io/rsocket/StreamIdSupplierPerf.java rename to benchmarks/src/main/java/io/rsocket/core/StreamIdSupplierPerf.java index c198b7a19..6b4f3f624 100644 --- a/benchmarks/src/main/java/io/rsocket/StreamIdSupplierPerf.java +++ b/benchmarks/src/main/java/io/rsocket/core/StreamIdSupplierPerf.java @@ -1,8 +1,16 @@ -package io.rsocket; +package io.rsocket.core; import io.netty.util.collection.IntObjectMap; import io.rsocket.internal.SynchronizedIntObjectHashMap; -import org.openjdk.jmh.annotations.*; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; @BenchmarkMode(Mode.Throughput) From 78082027c8e2590ed4af6db7af780dd44cc45917 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Tue, 28 Apr 2020 07:34:22 +0100 Subject: [PATCH 158/181] Deprecate ResponderRSocket (#802) --- .../main/java/io/rsocket/ResponderRSocket.java | 5 +++++ .../java/io/rsocket/core/RSocketResponder.java | 16 ++++++++++------ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/ResponderRSocket.java b/rsocket-core/src/main/java/io/rsocket/ResponderRSocket.java index f98901472..22697f130 100644 --- a/rsocket-core/src/main/java/io/rsocket/ResponderRSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/ResponderRSocket.java @@ -1,12 +1,17 @@ package io.rsocket; +import java.util.function.BiFunction; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; /** * Extends the {@link RSocket} that allows an implementer to peek at the first request payload of a * channel. + * + * @deprecated as of 1.0 RC7 in favor of using {@link RSocket#requestChannel(Publisher)} with {@link + * Flux#switchOnFirst(BiFunction)} */ +@Deprecated public interface ResponderRSocket extends RSocket { /** * Implement this method to peak at the first payload of the incoming request stream without diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index b5b298e14..f5c4aecec 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -27,7 +27,6 @@ import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.ResponderRSocket; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.frame.*; import io.rsocket.frame.decoder.PayloadDecoder; @@ -51,7 +50,7 @@ import reactor.util.concurrent.Queues; /** Responder side of RSocket. Receives {@link ByteBuf}s from a peer's {@link RSocketRequester} */ -class RSocketResponder implements ResponderRSocket { +class RSocketResponder implements RSocket { private static final Consumer DROPPED_ELEMENTS_CONSUMER = referenceCounted -> { if (referenceCounted.refCnt() > 0) { @@ -66,7 +65,10 @@ class RSocketResponder implements ResponderRSocket { private final DuplexConnection connection; private final RSocket requestHandler; - private final ResponderRSocket responderRSocket; + + @SuppressWarnings("deprecation") + private final io.rsocket.ResponderRSocket responderRSocket; + private final PayloadDecoder payloadDecoder; private final Consumer errorConsumer; private final ResponderLeaseHandler leaseHandler; @@ -86,6 +88,7 @@ class RSocketResponder implements ResponderRSocket { private final UnboundedProcessor sendProcessor; private final ByteBufAllocator allocator; + @SuppressWarnings("deprecation") RSocketResponder( DuplexConnection connection, RSocket requestHandler, @@ -99,7 +102,9 @@ class RSocketResponder implements ResponderRSocket { this.requestHandler = requestHandler; this.responderRSocket = - (requestHandler instanceof ResponderRSocket) ? (ResponderRSocket) requestHandler : null; + (requestHandler instanceof io.rsocket.ResponderRSocket) + ? (io.rsocket.ResponderRSocket) requestHandler + : null; this.payloadDecoder = payloadDecoder; this.errorConsumer = errorConsumer; @@ -219,8 +224,7 @@ public Flux requestChannel(Publisher payloads) { } } - @Override - public Flux requestChannel(Payload payload, Publisher payloads) { + private Flux requestChannel(Payload payload, Publisher payloads) { try { if (leaseHandler.useLease()) { return responderRSocket.requestChannel(payload, payloads); From 444709a131fc1e98a9c694e6cba20ad11b016cb7 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 28 Apr 2020 09:43:34 +0300 Subject: [PATCH 159/181] fixes code sample --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 0e055721e..8a2ba991c 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,6 @@ RSocket clientRSocket = // Enable Zero Copy .payloadDecoder(PayloadDecoder.ZERO_COPY) .connect(TcpClientTransport.create(7878)) - .start() .block(); ``` From 86ac4a0669f3aa70e9a107a454f06024b129bde4 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 28 Apr 2020 15:54:32 +0300 Subject: [PATCH 160/181] Removes TupleByteBufs (#804) --- .../rsocket/buffer/AbstractTupleByteBuf.java | 607 ------------------ .../java/io/rsocket/buffer/BufferUtil.java | 78 --- .../java/io/rsocket/buffer/Tuple2ByteBuf.java | 394 ------------ .../java/io/rsocket/buffer/Tuple3ByteBuf.java | 571 ---------------- .../java/io/rsocket/buffer/TupleByteBuf.java | 35 - .../io/rsocket/buffer/Tuple3ByteBufTest.java | 98 --- 6 files changed, 1783 deletions(-) delete mode 100644 rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/buffer/BufferUtil.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuf.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/buffer/Tuple3ByteBufTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java deleted file mode 100644 index a80605877..000000000 --- a/rsocket-core/src/main/java/io/rsocket/buffer/AbstractTupleByteBuf.java +++ /dev/null @@ -1,607 +0,0 @@ -package io.rsocket.buffer; - -import io.netty.buffer.AbstractReferenceCountedByteBuf; -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.netty.util.internal.SystemPropertyUtil; -import java.io.InputStream; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.nio.channels.FileChannel; -import java.nio.channels.ScatteringByteChannel; -import java.nio.charset.Charset; - -abstract class AbstractTupleByteBuf extends AbstractReferenceCountedByteBuf { - static final int DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT = - SystemPropertyUtil.getInt("io.netty.allocator.directMemoryCacheAlignment", 0); - static final ByteBuffer EMPTY_NIO_BUFFER = Unpooled.EMPTY_BUFFER.nioBuffer(); - static final int NOT_ENOUGH_BYTES_AT_MAX_CAPACITY_CODE = 3; - - final ByteBufAllocator allocator; - final int capacity; - - AbstractTupleByteBuf(ByteBufAllocator allocator, int capacity) { - super(Integer.MAX_VALUE); - - this.capacity = capacity; - this.allocator = allocator; - super.writerIndex(capacity); - } - - abstract long calculateRelativeIndex(int index); - - abstract ByteBuf getPart(int index); - - @Override - public ByteBuffer nioBuffer(int index, int length) { - checkIndex(index, length); - - ByteBuffer[] buffers = nioBuffers(index, length); - - if (buffers.length == 1) { - return buffers[0].duplicate(); - } - - ByteBuffer merged = - BufferUtil.allocateDirectAligned(length, DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT) - .order(order()); - for (ByteBuffer buf : buffers) { - merged.put(buf); - } - - merged.flip(); - return merged; - } - - @Override - public ByteBuffer[] nioBuffers(int index, int length) { - checkIndex(index, length); - if (length == 0) { - return new ByteBuffer[] {EMPTY_NIO_BUFFER}; - } - return _nioBuffers(index, length); - } - - protected abstract ByteBuffer[] _nioBuffers(int index, int length); - - @Override - protected byte _getByte(final int index) { - long ri = calculateRelativeIndex(index); - ByteBuf byteBuf = getPart(index); - - int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - - return byteBuf.getByte(calculatedIndex); - } - - @Override - protected short _getShort(final int index) { - long ri = calculateRelativeIndex(index); - ByteBuf byteBuf = getPart(index); - - final int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - - if (calculatedIndex + Short.BYTES <= byteBuf.writerIndex()) { - return byteBuf.getShort(calculatedIndex); - } else if (order() == ByteOrder.BIG_ENDIAN) { - return (short) ((_getByte(index) & 0xff) << 8 | _getByte(index + 1) & 0xff); - } else { - return (short) (_getByte(index) & 0xff | (_getByte(index + 1) & 0xff) << 8); - } - } - - @Override - protected short _getShortLE(int index) { - long ri = calculateRelativeIndex(index); - ByteBuf byteBuf = getPart(index); - - final int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - - if (calculatedIndex + Short.BYTES <= byteBuf.writerIndex()) { - return byteBuf.getShortLE(calculatedIndex); - } else if (order() == ByteOrder.BIG_ENDIAN) { - return (short) (_getByte(index) & 0xff | (_getByte(index + 1) & 0xff) << 8); - } else { - return (short) ((_getByte(index) & 0xff) << 8 | _getByte(index + 1) & 0xff); - } - } - - @Override - protected int _getUnsignedMedium(final int index) { - long ri = calculateRelativeIndex(index); - ByteBuf byteBuf = getPart(index); - - int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - - if (calculatedIndex + 3 <= byteBuf.writerIndex()) { - return byteBuf.getUnsignedMedium(calculatedIndex); - } else if (order() == ByteOrder.BIG_ENDIAN) { - return (_getShort(index) & 0xffff) << 8 | _getByte(index + 2) & 0xff; - } else { - return _getShort(index) & 0xFFFF | (_getByte(index + 2) & 0xFF) << 16; - } - } - - @Override - protected int _getUnsignedMediumLE(int index) { - long ri = calculateRelativeIndex(index); - ByteBuf byteBuf = getPart(index); - - int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - - if (calculatedIndex + 3 <= byteBuf.writerIndex()) { - return byteBuf.getUnsignedMediumLE(calculatedIndex); - } else if (order() == ByteOrder.BIG_ENDIAN) { - return _getShortLE(index) & 0xffff | (_getByte(index + 2) & 0xff) << 16; - } else { - return (_getShortLE(index) & 0xffff) << 8 | _getByte(index + 2) & 0xff; - } - } - - @Override - protected int _getInt(final int index) { - long ri = calculateRelativeIndex(index); - ByteBuf byteBuf = getPart(index); - - int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - - if (calculatedIndex + Integer.BYTES <= byteBuf.writerIndex()) { - return byteBuf.getInt(calculatedIndex); - } else if (order() == ByteOrder.BIG_ENDIAN) { - return (_getShort(index) & 0xffff) << 16 | _getShort(index + 2) & 0xffff; - } else { - return _getShort(index) & 0xFFFF | (_getShort(index + 2) & 0xFFFF) << 16; - } - } - - @Override - protected int _getIntLE(final int index) { - long ri = calculateRelativeIndex(index); - ByteBuf byteBuf = getPart(index); - - int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - - if (calculatedIndex + Integer.BYTES <= byteBuf.writerIndex()) { - return byteBuf.getIntLE(calculatedIndex); - } else if (order() == ByteOrder.BIG_ENDIAN) { - return _getShortLE(index) & 0xffff | (_getShortLE(index + 2) & 0xffff) << 16; - } else { - return (_getShortLE(index) & 0xffff) << 16 | _getShortLE(index + 2) & 0xffff; - } - } - - @Override - protected long _getLong(final int index) { - long ri = calculateRelativeIndex(index); - ByteBuf byteBuf = getPart(index); - - int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - - if (calculatedIndex + Long.BYTES <= byteBuf.writerIndex()) { - return byteBuf.getLong(calculatedIndex); - } else if (order() == ByteOrder.BIG_ENDIAN) { - return (_getInt(index) & 0xffffffffL) << 32 | _getInt(index + 4) & 0xffffffffL; - } else { - return _getInt(index) & 0xFFFFFFFFL | (_getInt(index + 4) & 0xFFFFFFFFL) << 32; - } - } - - @Override - protected long _getLongLE(final int index) { - long ri = calculateRelativeIndex(index); - ByteBuf byteBuf = getPart(index); - - int calculatedIndex = (int) (ri & Integer.MAX_VALUE); - - if (calculatedIndex + Long.BYTES <= byteBuf.writerIndex()) { - return byteBuf.getLongLE(calculatedIndex); - } else if (order() == ByteOrder.BIG_ENDIAN) { - return (_getInt(index) & 0xffffffffL) << 32 | _getInt(index + 4) & 0xffffffffL; - } else { - return _getInt(index) & 0xFFFFFFFFL | (_getInt(index + 4) & 0xFFFFFFFFL) << 32; - } - } - - @Override - public ByteBufAllocator alloc() { - return allocator; - } - - @Override - public int capacity() { - return capacity; - } - - @Override - public ByteBuf capacity(int newCapacity) { - throw new UnsupportedOperationException(); - } - - @Override - public int maxCapacity() { - return capacity; - } - - @Override - public ByteOrder order() { - return ByteOrder.LITTLE_ENDIAN; - } - - @Override - public ByteBuf order(ByteOrder endianness) { - return this; - } - - @Override - public ByteBuf unwrap() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean isReadOnly() { - return true; - } - - @Override - public ByteBuf asReadOnly() { - return this; - } - - @Override - public boolean isWritable() { - return false; - } - - @Override - public boolean isWritable(int size) { - return false; - } - - @Override - public ByteBuf writerIndex(int writerIndex) { - return this; - } - - @Override - public final int writerIndex() { - return capacity; - } - - @Override - public ByteBuf setIndex(int readerIndex, int writerIndex) { - return this; - } - - @Override - public ByteBuf clear() { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf discardReadBytes() { - return this; - } - - @Override - public ByteBuf discardSomeReadBytes() { - return this; - } - - @Override - public ByteBuf ensureWritable(int minWritableBytes) { - return this; - } - - @Override - public int ensureWritable(int minWritableBytes, boolean force) { - return NOT_ENOUGH_BYTES_AT_MAX_CAPACITY_CODE; - } - - @Override - public ByteBuf setFloatLE(int index, float value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setDoubleLE(int index, double value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setBoolean(int index, boolean value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setByte(int index, int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setShort(int index, int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setShortLE(int index, int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setMedium(int index, int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setMediumLE(int index, int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setInt(int index, int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setIntLE(int index, int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setLong(int index, long value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setLongLE(int index, long value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setChar(int index, int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setFloat(int index, float value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setDouble(int index, double value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setBytes(int index, ByteBuf src) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setBytes(int index, ByteBuf src, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setBytes(int index, ByteBuf src, int srcIndex, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setBytes(int index, byte[] src) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setBytes(int index, byte[] src, int srcIndex, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setBytes(int index, ByteBuffer src) { - throw new UnsupportedOperationException(); - } - - @Override - public int setBytes(int index, InputStream in, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public int setBytes(int index, ScatteringByteChannel in, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public int setBytes(int index, FileChannel in, long position, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public int setCharSequence(int index, CharSequence sequence, Charset charset) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf setZero(int index, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeBoolean(boolean value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeByte(int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeShort(int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeShortLE(int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeMedium(int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeMediumLE(int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeInt(int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeIntLE(int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeLong(long value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeLongLE(long value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeChar(int value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeFloat(float value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeDouble(double value) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeBytes(ByteBuf src) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeBytes(ByteBuf src, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeBytes(ByteBuf src, int srcIndex, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeBytes(byte[] src) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeBytes(byte[] src, int srcIndex, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeBytes(ByteBuffer src) { - throw new UnsupportedOperationException(); - } - - @Override - public int writeBytes(InputStream in, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public int writeBytes(ScatteringByteChannel in, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public int writeBytes(FileChannel in, long position, int length) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuf writeZero(int length) { - throw new UnsupportedOperationException(); - } - - @Override - public int writeCharSequence(CharSequence sequence, Charset charset) { - throw new UnsupportedOperationException(); - } - - @Override - public ByteBuffer internalNioBuffer(int index, int length) { - return nioBuffer(index, length); - } - - @Override - public boolean hasArray() { - return false; - } - - @Override - public byte[] array() { - throw new UnsupportedOperationException(); - } - - @Override - public int arrayOffset() { - throw new UnsupportedOperationException(); - } - - @Override - public boolean hasMemoryAddress() { - return false; - } - - @Override - public long memoryAddress() { - throw new UnsupportedOperationException(); - } - - @Override - protected void _setByte(int index, int value) {} - - @Override - protected void _setShort(int index, int value) {} - - @Override - protected void _setShortLE(int index, int value) {} - - @Override - protected void _setMedium(int index, int value) {} - - @Override - protected void _setMediumLE(int index, int value) {} - - @Override - protected void _setInt(int index, int value) {} - - @Override - protected void _setIntLE(int index, int value) {} - - @Override - protected void _setLong(int index, long value) {} - - @Override - protected void _setLongLE(int index, long value) {} -} diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/BufferUtil.java b/rsocket-core/src/main/java/io/rsocket/buffer/BufferUtil.java deleted file mode 100644 index 476583ab3..000000000 --- a/rsocket-core/src/main/java/io/rsocket/buffer/BufferUtil.java +++ /dev/null @@ -1,78 +0,0 @@ -package io.rsocket.buffer; - -import java.lang.reflect.Field; -import java.nio.Buffer; -import java.nio.ByteBuffer; -import java.security.AccessController; -import java.security.PrivilegedExceptionAction; -import sun.misc.Unsafe; - -abstract class BufferUtil { - - private static final Unsafe UNSAFE; - - static { - Unsafe unsafe; - try { - final PrivilegedExceptionAction action = - () -> { - final Field f = Unsafe.class.getDeclaredField("theUnsafe"); - f.setAccessible(true); - - return (Unsafe) f.get(null); - }; - - unsafe = AccessController.doPrivileged(action); - } catch (final Exception ex) { - throw new RuntimeException(ex); - } - - UNSAFE = unsafe; - } - - private static final long BYTE_BUFFER_ADDRESS_FIELD_OFFSET; - - static { - try { - BYTE_BUFFER_ADDRESS_FIELD_OFFSET = - UNSAFE.objectFieldOffset(Buffer.class.getDeclaredField("address")); - } catch (final Exception ex) { - throw new RuntimeException(ex); - } - } - - /** - * Allocate a new direct {@link ByteBuffer} that is aligned on a given alignment boundary. - * - * @param capacity required for the buffer. - * @param alignment boundary at which the buffer should begin. - * @return a new {@link ByteBuffer} with the required alignment. - * @throws IllegalArgumentException if the alignment is not a power of 2. - */ - static ByteBuffer allocateDirectAligned(final int capacity, final int alignment) { - if (alignment == 0) { - return ByteBuffer.allocateDirect(capacity); - } - - if (!isPowerOfTwo(alignment)) { - throw new IllegalArgumentException("Must be a power of 2: alignment=" + alignment); - } - - final ByteBuffer buffer = ByteBuffer.allocateDirect(capacity + alignment); - - final long address = UNSAFE.getLong(buffer, BYTE_BUFFER_ADDRESS_FIELD_OFFSET); - final int remainder = (int) (address & (alignment - 1)); - final int offset = alignment - remainder; - - buffer.limit(capacity + offset); - buffer.position(offset); - - return buffer.slice(); - } - - private static boolean isPowerOfTwo(final int value) { - return value > 0 && ((value & (~value + 1)) == value); - } - - private BufferUtil() {} -} diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java deleted file mode 100644 index ba6620cb0..000000000 --- a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple2ByteBuf.java +++ /dev/null @@ -1,394 +0,0 @@ -package io.rsocket.buffer; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.netty.util.ReferenceCountUtil; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.GatheringByteChannel; -import java.nio.charset.Charset; - -class Tuple2ByteBuf extends AbstractTupleByteBuf { - - private static final long ONE_MASK = 0x100000000L; - private static final long TWO_MASK = 0x200000000L; - private static final long MASK = 0x700000000L; - - private final ByteBuf one; - private final ByteBuf two; - private final int oneReadIndex; - private final int twoReadIndex; - private final int oneReadableBytes; - private final int twoReadableBytes; - private final int twoRelativeIndex; - - private boolean freed; - - Tuple2ByteBuf(ByteBufAllocator allocator, ByteBuf one, ByteBuf two) { - super(allocator, one.readableBytes() + two.readableBytes()); - - this.one = one; - this.two = two; - - this.oneReadIndex = one.readerIndex(); - this.twoReadIndex = two.readerIndex(); - - this.oneReadableBytes = one.readableBytes(); - this.twoReadableBytes = two.readableBytes(); - - this.twoRelativeIndex = oneReadableBytes; - - this.freed = false; - } - - @Override - long calculateRelativeIndex(int index) { - checkIndex(index, 0); - - long relativeIndex; - long mask; - if (index >= twoRelativeIndex) { - relativeIndex = twoReadIndex + (index - oneReadableBytes); - mask = TWO_MASK; - } else { - relativeIndex = oneReadIndex + index; - mask = ONE_MASK; - } - - return relativeIndex | mask; - } - - @Override - ByteBuf getPart(int index) { - long ri = calculateRelativeIndex(index); - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - return one; - case 0x2: - return two; - default: - throw new IllegalStateException(); - } - } - - @Override - public boolean isDirect() { - return one.isDirect() && two.isDirect(); - } - - @Override - public int nioBufferCount() { - return one.nioBufferCount() + two.nioBufferCount(); - } - - @Override - public ByteBuffer nioBuffer() { - ByteBuffer[] oneBuffers = one.nioBuffers(); - ByteBuffer[] twoBuffers = two.nioBuffers(); - - ByteBuffer merged = - BufferUtil.allocateDirectAligned(capacity, DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT) - .order(order()); - - for (ByteBuffer b : oneBuffers) { - merged.put(b); - } - - for (ByteBuffer b : twoBuffers) { - merged.put(b); - } - - merged.flip(); - return merged; - } - - @Override - public ByteBuffer[] _nioBuffers(int index, int length) { - long ri = calculateRelativeIndex(index); - index = (int) (ri & Integer.MAX_VALUE); - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - ByteBuffer[] oneBuffer; - ByteBuffer[] twoBuffer; - int l = Math.min(oneReadableBytes - index, length); - oneBuffer = one.nioBuffers(index, l); - length -= l; - if (length != 0) { - twoBuffer = two.nioBuffers(twoReadIndex, length); - ByteBuffer[] results = new ByteBuffer[oneBuffer.length + twoBuffer.length]; - System.arraycopy(oneBuffer, 0, results, 0, oneBuffer.length); - System.arraycopy(twoBuffer, 0, results, oneBuffer.length, twoBuffer.length); - return results; - } else { - return oneBuffer; - } - case 0x2: - return two.nioBuffers(index, length); - default: - throw new IllegalStateException(); - } - } - - @Override - public ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) { - checkDstIndex(index, length, dstIndex, dst.capacity()); - if (length == 0) { - return this; - } - - // FIXME: check twice here - long ri = calculateRelativeIndex(index); - index = (int) (ri & Integer.MAX_VALUE); - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - { - int l = Math.min(oneReadableBytes - index, length); - one.getBytes(index, dst, dstIndex, l); - length -= l; - dstIndex += l; - - if (length != 0) { - two.getBytes(twoReadIndex, dst, dstIndex, length); - } - - break; - } - case 0x2: - { - two.getBytes(index, dst, dstIndex, length); - break; - } - default: - throw new IllegalStateException(); - } - - return this; - } - - @Override - public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) { - ByteBuf dstBuf = Unpooled.wrappedBuffer(dst); - return getBytes(index, dstBuf, dstIndex, length); - } - - @Override - public ByteBuf getBytes(int index, ByteBuffer dst) { - ByteBuf dstBuf = Unpooled.wrappedBuffer(dst); - return getBytes(index, dstBuf); - } - - @Override - public ByteBuf getBytes(int index, final OutputStream out, int length) throws IOException { - checkIndex(index, length); - if (length == 0) { - return this; - } - - long ri = calculateRelativeIndex(index); - index = (int) (ri & Integer.MAX_VALUE); - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - { - int l = Math.min(oneReadableBytes - index, length); - one.getBytes(index, out, l); - length -= l; - if (length != 0) { - two.getBytes(twoReadIndex, out, length); - } - break; - } - case 0x2: - { - two.getBytes(index, out, length); - break; - } - default: - throw new IllegalStateException(); - } - - return this; - } - - @Override - public int getBytes(int index, GatheringByteChannel out, int length) throws IOException { - checkIndex(index, length); - int read = 0; - long ri = calculateRelativeIndex(index); - index = (int) (ri & Integer.MAX_VALUE); - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - { - int l = Math.min(oneReadableBytes - index, length); - read += one.getBytes(index, out, l); - length -= l; - if (length != 0) { - read += two.getBytes(twoReadIndex, out, length); - } - break; - } - case 0x2: - { - read += two.getBytes(index, out, length); - break; - } - default: - throw new IllegalStateException(); - } - - return read; - } - - @Override - public int getBytes(int index, FileChannel out, long position, int length) throws IOException { - checkIndex(index, length); - int read = 0; - long ri = calculateRelativeIndex(index); - index = (int) (ri & Integer.MAX_VALUE); - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - { - int l = Math.min(oneReadableBytes - index, length); - read += one.getBytes(index, out, position, l); - length -= l; - position += l; - if (length != 0) { - read += two.getBytes(twoReadIndex, out, position, length); - } - break; - } - case 0x2: - { - read += two.getBytes(index, out, position, length); - break; - } - default: - throw new IllegalStateException(); - } - - return read; - } - - @Override - public ByteBuf copy(int index, int length) { - checkIndex(index, length); - - ByteBuf buffer = allocator.buffer(length); - - if (index == 0 && length == capacity) { - buffer.writeBytes(one, oneReadIndex, oneReadableBytes); - buffer.writeBytes(two, twoReadIndex, twoReadableBytes); - - return buffer; - } - - long ri = calculateRelativeIndex(index); - index = (int) (ri & Integer.MAX_VALUE); - - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - { - int l = Math.min(oneReadableBytes - index, length); - buffer.writeBytes(one, index, l); - - length -= l; - - if (length != 0) { - buffer.writeBytes(two, twoReadIndex, length); - } - - return buffer; - } - case 0x2: - { - return buffer.writeBytes(two, index, length); - } - default: - throw new IllegalStateException(); - } - } - - @Override - public ByteBuf slice(final int readIndex, int length) { - checkIndex(readIndex, length); - - if (readIndex == 0 && length == capacity) { - return new Tuple2ByteBuf( - allocator, - one.slice(oneReadIndex, oneReadableBytes), - two.slice(twoReadIndex, twoReadableBytes)); - } - - long ri = calculateRelativeIndex(readIndex); - int index = (int) (ri & Integer.MAX_VALUE); - - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - { - ByteBuf oneSlice; - ByteBuf twoSlice; - - int l = Math.min(oneReadableBytes - index, length); - oneSlice = one.slice(index, l); - length -= l; - if (length != 0) { - twoSlice = two.slice(twoReadIndex, length); - return new Tuple2ByteBuf(allocator, oneSlice, twoSlice); - } else { - return oneSlice; - } - } - case 0x2: - { - return two.slice(index, length); - } - default: - throw new IllegalStateException(); - } - } - - @Override - protected void deallocate() { - if (freed) { - return; - } - - freed = true; - ReferenceCountUtil.safeRelease(one); - ReferenceCountUtil.safeRelease(two); - } - - @Override - public String toString(Charset charset) { - StringBuilder builder = new StringBuilder(capacity); - builder.append(one.toString(charset)); - builder.append(two.toString(charset)); - return builder.toString(); - } - - @Override - public String toString() { - return "Tuple2ByteBuf{" - + "capacity=" - + capacity - + ", one=" - + one - + ", two=" - + two - + ", allocator=" - + allocator - + ", oneReadIndex=" - + oneReadIndex - + ", twoReadIndex=" - + twoReadIndex - + ", oneReadableBytes=" - + oneReadableBytes - + ", twoReadableBytes=" - + twoReadableBytes - + ", twoRelativeIndex=" - + twoRelativeIndex - + '}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java deleted file mode 100644 index be593019f..000000000 --- a/rsocket-core/src/main/java/io/rsocket/buffer/Tuple3ByteBuf.java +++ /dev/null @@ -1,571 +0,0 @@ -package io.rsocket.buffer; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.netty.util.ReferenceCountUtil; -import java.io.IOException; -import java.io.OutputStream; -import java.nio.ByteBuffer; -import java.nio.channels.FileChannel; -import java.nio.channels.GatheringByteChannel; -import java.nio.charset.Charset; - -class Tuple3ByteBuf extends AbstractTupleByteBuf { - private static final long ONE_MASK = 0x100000000L; - private static final long TWO_MASK = 0x200000000L; - private static final long THREE_MASK = 0x400000000L; - private static final long MASK = 0x700000000L; - - private final ByteBuf one; - private final ByteBuf two; - private final ByteBuf three; - private final int oneReadIndex; - private final int twoReadIndex; - private final int threeReadIndex; - private final int oneReadableBytes; - private final int twoReadableBytes; - private final int threeReadableBytes; - private final int twoRelativeIndex; - private final int threeRelativeIndex; - - private boolean freed; - - Tuple3ByteBuf(ByteBufAllocator allocator, ByteBuf one, ByteBuf two, ByteBuf three) { - super(allocator, one.readableBytes() + two.readableBytes() + three.readableBytes()); - - this.one = one; - this.two = two; - this.three = three; - - this.oneReadIndex = one.readerIndex(); - this.twoReadIndex = two.readerIndex(); - this.threeReadIndex = three.readerIndex(); - - this.oneReadableBytes = one.readableBytes(); - this.twoReadableBytes = two.readableBytes(); - this.threeReadableBytes = three.readableBytes(); - - this.twoRelativeIndex = oneReadableBytes; - this.threeRelativeIndex = twoRelativeIndex + twoReadableBytes; - - this.freed = false; - } - - @Override - public boolean isDirect() { - return one.isDirect() && two.isDirect() && three.isDirect(); - } - - @Override - public long calculateRelativeIndex(int index) { - checkIndex(index, 0); - long relativeIndex; - long mask; - if (index >= threeRelativeIndex) { - relativeIndex = threeReadIndex + (index - twoReadableBytes - oneReadableBytes); - mask = THREE_MASK; - } else if (index >= twoRelativeIndex) { - relativeIndex = twoReadIndex + (index - oneReadableBytes); - mask = TWO_MASK; - } else { - relativeIndex = oneReadIndex + index; - mask = ONE_MASK; - } - - return relativeIndex | mask; - } - - @Override - public ByteBuf getPart(int index) { - long ri = calculateRelativeIndex(index); - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - return one; - case 0x2: - return two; - case 0x4: - return three; - default: - throw new IllegalStateException(); - } - } - - @Override - public int nioBufferCount() { - return one.nioBufferCount() + two.nioBufferCount() + three.nioBufferCount(); - } - - @Override - public ByteBuffer nioBuffer() { - - ByteBuffer[] oneBuffers = one.nioBuffers(); - ByteBuffer[] twoBuffers = two.nioBuffers(); - ByteBuffer[] threeBuffers = three.nioBuffers(); - - ByteBuffer merged = - BufferUtil.allocateDirectAligned(capacity, DEFAULT_DIRECT_MEMORY_CACHE_ALIGNMENT) - .order(order()); - - for (ByteBuffer b : oneBuffers) { - merged.put(b); - } - - for (ByteBuffer b : twoBuffers) { - merged.put(b); - } - - for (ByteBuffer b : threeBuffers) { - merged.put(b); - } - - merged.flip(); - return merged; - } - - @Override - public ByteBuffer[] _nioBuffers(int index, int length) { - long ri = calculateRelativeIndex(index); - index = (int) (ri & Integer.MAX_VALUE); - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - { - ByteBuffer[] oneBuffer; - ByteBuffer[] twoBuffer; - ByteBuffer[] threeBuffer; - int l = Math.min(oneReadableBytes - index, length); - oneBuffer = one.nioBuffers(index, l); - length -= l; - if (length != 0) { - l = Math.min(twoReadableBytes, length); - twoBuffer = two.nioBuffers(twoReadIndex, l); - length -= l; - if (length != 0) { - threeBuffer = three.nioBuffers(threeReadIndex, length); - ByteBuffer[] results = - new ByteBuffer[oneBuffer.length + twoBuffer.length + threeBuffer.length]; - System.arraycopy(oneBuffer, 0, results, 0, oneBuffer.length); - System.arraycopy(twoBuffer, 0, results, oneBuffer.length, twoBuffer.length); - System.arraycopy( - threeBuffer, 0, results, oneBuffer.length + twoBuffer.length, threeBuffer.length); - return results; - } else { - ByteBuffer[] results = new ByteBuffer[oneBuffer.length + twoBuffer.length]; - System.arraycopy(oneBuffer, 0, results, 0, oneBuffer.length); - System.arraycopy(twoBuffer, 0, results, oneBuffer.length, twoBuffer.length); - return results; - } - } else { - return oneBuffer; - } - } - case 0x2: - { - ByteBuffer[] twoBuffer; - ByteBuffer[] threeBuffer; - int l = Math.min(twoReadableBytes - index, length); - twoBuffer = two.nioBuffers(index, l); - length -= l; - if (length != 0) { - threeBuffer = three.nioBuffers(threeReadIndex, length); - ByteBuffer[] results = new ByteBuffer[twoBuffer.length + threeBuffer.length]; - System.arraycopy(twoBuffer, 0, results, 0, twoBuffer.length); - System.arraycopy(threeBuffer, 0, results, twoBuffer.length, threeBuffer.length); - return results; - } else { - return twoBuffer; - } - } - case 0x4: - return three.nioBuffers(index, length); - default: - throw new IllegalStateException(); - } - } - - @Override - public ByteBuf getBytes(int index, ByteBuf dst, int dstIndex, int length) { - checkDstIndex(index, length, dstIndex, dst.capacity()); - long ri = calculateRelativeIndex(index); - index = (int) (ri & Integer.MAX_VALUE); - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - { - int l = Math.min(oneReadableBytes - index, length); - one.getBytes(index, dst, dstIndex, l); - length -= l; - dstIndex += l; - - if (length != 0) { - l = Math.min(twoReadableBytes, length); - two.getBytes(twoReadIndex, dst, dstIndex, l); - length -= l; - dstIndex += l; - - if (length != 0) { - three.getBytes(threeReadIndex, dst, dstIndex, length); - } - } - break; - } - case 0x2: - { - int l = Math.min(twoReadableBytes - index, length); - two.getBytes(index, dst, dstIndex, l); - length -= l; - dstIndex += l; - - if (length != 0) { - three.getBytes(threeReadIndex, dst, dstIndex, length); - } - break; - } - case 0x4: - { - three.getBytes(index, dst, dstIndex, length); - break; - } - default: - throw new IllegalStateException(); - } - - return this; - } - - @Override - public ByteBuf getBytes(int index, byte[] dst, int dstIndex, int length) { - ByteBuf dstBuf = Unpooled.wrappedBuffer(dst); - return getBytes(index, dstBuf, dstIndex, length); - } - - @Override - public ByteBuf getBytes(int index, ByteBuffer dst) { - ByteBuf dstBuf = Unpooled.wrappedBuffer(dst); - return getBytes(index, dstBuf); - } - - @Override - public ByteBuf getBytes(int index, final OutputStream out, int length) throws IOException { - checkIndex(index, length); - long ri = calculateRelativeIndex(index); - index = (int) (ri & Integer.MAX_VALUE); - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - { - int l = Math.min(oneReadableBytes - index, length); - one.getBytes(index, out, l); - length -= l; - if (length != 0) { - l = Math.min(twoReadableBytes, length); - two.getBytes(twoReadIndex, out, l); - length -= l; - if (length != 0) { - three.getBytes(threeReadIndex, out, length); - } - } - break; - } - case 0x2: - { - int l = Math.min(twoReadableBytes - index, length); - two.getBytes(index, out, l); - length -= l; - - if (length != 0) { - three.getBytes(threeReadIndex, out, length); - } - break; - } - case 0x4: - { - three.getBytes(index, out, length); - - break; - } - default: - throw new IllegalStateException(); - } - - return this; - } - - @Override - public int getBytes(int index, GatheringByteChannel out, int length) throws IOException { - checkIndex(index, length); - int read = 0; - long ri = calculateRelativeIndex(index); - index = (int) (ri & Integer.MAX_VALUE); - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - { - int l = Math.min(oneReadableBytes - index, length); - read += one.getBytes(index, out, l); - length -= l; - if (length != 0) { - l = Math.min(twoReadableBytes, length); - read += two.getBytes(twoReadIndex, out, l); - length -= l; - if (length != 0) { - read += three.getBytes(threeReadIndex, out, length); - } - } - break; - } - case 0x2: - { - int l = Math.min(twoReadableBytes - index, length); - read += two.getBytes(index, out, l); - length -= l; - - if (length != 0) { - read += three.getBytes(threeReadIndex, out, length); - } - break; - } - case 0x4: - { - read += three.getBytes(index, out, length); - - break; - } - default: - throw new IllegalStateException(); - } - - return read; - } - - @Override - public int getBytes(int index, FileChannel out, long position, int length) throws IOException { - checkIndex(index, length); - int read = 0; - long ri = calculateRelativeIndex(index); - index = (int) (ri & Integer.MAX_VALUE); - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - { - int l = Math.min(oneReadableBytes - index, length); - read += one.getBytes(index, out, position, l); - length -= l; - position += l; - - if (length != 0) { - l = Math.min(twoReadableBytes, length); - read += two.getBytes(twoReadIndex, out, position, l); - length -= l; - position += l; - - if (length != 0) { - read += three.getBytes(threeReadIndex, out, position, length); - } - } - break; - } - case 0x2: - { - int l = Math.min(twoReadableBytes - index, length); - read += two.getBytes(index, out, position, l); - length -= l; - position += l; - - if (length != 0) { - read += three.getBytes(threeReadIndex, out, position, length); - } - break; - } - case 0x4: - { - read += three.getBytes(index, out, position, length); - - break; - } - default: - throw new IllegalStateException(); - } - - return read; - } - - @Override - public ByteBuf copy(int index, int length) { - checkIndex(index, length); - - ByteBuf buffer = allocator.buffer(length); - - if (index == 0 && length == capacity) { - buffer.writeBytes(one, oneReadIndex, oneReadableBytes); - buffer.writeBytes(two, twoReadIndex, twoReadableBytes); - buffer.writeBytes(three, threeReadIndex, threeReadableBytes); - - return buffer; - } - - long ri = calculateRelativeIndex(index); - index = (int) (ri & Integer.MAX_VALUE); - - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - { - int l = Math.min(oneReadableBytes - index, length); - buffer.writeBytes(one, index, l); - length -= l; - - if (length != 0) { - l = Math.min(twoReadableBytes, length); - buffer.writeBytes(two, twoReadIndex, l); - length -= l; - if (length != 0) { - buffer.writeBytes(three, threeReadIndex, length); - } - } - - return buffer; - } - case 0x2: - { - int l = Math.min(twoReadableBytes - index, length); - buffer.writeBytes(two, index, l); - length -= l; - - if (length != 0) { - buffer.writeBytes(three, threeReadIndex, length); - } - - return buffer; - } - case 0x4: - { - buffer.writeBytes(three, index, length); - - return buffer; - } - default: - throw new IllegalStateException(); - } - } - - @Override - public ByteBuf retainedSlice() { - return new Tuple3ByteBuf( - allocator, - one.retainedSlice(oneReadIndex, oneReadableBytes), - two.retainedSlice(twoReadIndex, twoReadableBytes), - three.retainedSlice(threeReadIndex, threeReadableBytes)); - } - - @Override - public ByteBuf slice(final int readIndex, int length) { - checkIndex(readIndex, length); - - if (readIndex == 0 && length == capacity) { - return new Tuple3ByteBuf( - allocator, - one.slice(oneReadIndex, oneReadableBytes), - two.slice(twoReadIndex, twoReadableBytes), - three.slice(threeReadIndex, threeReadableBytes)); - } - - long ri = calculateRelativeIndex(readIndex); - int index = (int) (ri & Integer.MAX_VALUE); - switch ((int) ((ri & MASK) >>> 32L)) { - case 0x1: - { - ByteBuf oneSlice; - ByteBuf twoSlice; - ByteBuf threeSlice; - - int l = Math.min(oneReadableBytes - index, length); - oneSlice = one.slice(index, l); - length -= l; - if (length != 0) { - l = Math.min(twoReadableBytes, length); - twoSlice = two.slice(twoReadIndex, l); - length -= l; - if (length != 0) { - threeSlice = three.slice(threeReadIndex, length); - return new Tuple3ByteBuf(allocator, oneSlice, twoSlice, threeSlice); - } else { - return new Tuple2ByteBuf(allocator, oneSlice, twoSlice); - } - - } else { - return oneSlice; - } - } - case 0x2: - { - ByteBuf twoSlice; - ByteBuf threeSlice; - - int l = Math.min(twoReadableBytes - index, length); - twoSlice = two.slice(index, l); - length -= l; - if (length != 0) { - threeSlice = three.slice(threeReadIndex, length); - return new Tuple2ByteBuf(allocator, twoSlice, threeSlice); - } else { - return twoSlice; - } - } - case 0x4: - { - return three.slice(index, length); - } - default: - throw new IllegalStateException(); - } - } - - @Override - protected void deallocate() { - if (freed) { - return; - } - - freed = true; - ReferenceCountUtil.safeRelease(one); - ReferenceCountUtil.safeRelease(two); - ReferenceCountUtil.safeRelease(three); - } - - @Override - public String toString(Charset charset) { - StringBuilder builder = new StringBuilder(3); - builder.append(one.toString(charset)); - builder.append(two.toString(charset)); - builder.append(three.toString(charset)); - return builder.toString(); - } - - @Override - public String toString() { - return "Tuple3ByteBuf{" - + "capacity=" - + capacity - + ", one=" - + one - + ", two=" - + two - + ", three=" - + three - + ", allocator=" - + allocator - + ", oneReadIndex=" - + oneReadIndex - + ", twoReadIndex=" - + twoReadIndex - + ", threeReadIndex=" - + threeReadIndex - + ", oneReadableBytes=" - + oneReadableBytes - + ", twoReadableBytes=" - + twoReadableBytes - + ", threeReadableBytes=" - + threeReadableBytes - + ", twoRelativeIndex=" - + twoRelativeIndex - + ", threeRelativeIndex=" - + threeRelativeIndex - + '}'; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuf.java b/rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuf.java deleted file mode 100644 index 8c8e2e7e4..000000000 --- a/rsocket-core/src/main/java/io/rsocket/buffer/TupleByteBuf.java +++ /dev/null @@ -1,35 +0,0 @@ -package io.rsocket.buffer; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import java.util.Objects; - -public abstract class TupleByteBuf { - - private TupleByteBuf() {} - - public static ByteBuf of(ByteBuf one, ByteBuf two) { - return of(ByteBufAllocator.DEFAULT, one, two); - } - - public static ByteBuf of(ByteBufAllocator allocator, ByteBuf one, ByteBuf two) { - Objects.requireNonNull(allocator); - Objects.requireNonNull(one); - Objects.requireNonNull(two); - - return new Tuple2ByteBuf(allocator, one, two); - } - - public static ByteBuf of(ByteBuf one, ByteBuf two, ByteBuf three) { - return of(ByteBufAllocator.DEFAULT, one, two, three); - } - - public static ByteBuf of(ByteBufAllocator allocator, ByteBuf one, ByteBuf two, ByteBuf three) { - Objects.requireNonNull(allocator); - Objects.requireNonNull(one); - Objects.requireNonNull(two); - Objects.requireNonNull(three); - - return new Tuple3ByteBuf(allocator, one, two, three); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/buffer/Tuple3ByteBufTest.java b/rsocket-core/src/test/java/io/rsocket/buffer/Tuple3ByteBufTest.java deleted file mode 100644 index 4515fb29b..000000000 --- a/rsocket-core/src/test/java/io/rsocket/buffer/Tuple3ByteBufTest.java +++ /dev/null @@ -1,98 +0,0 @@ -package io.rsocket.buffer; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.Unpooled; -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.concurrent.ThreadLocalRandom; -import org.junit.Assert; -import org.junit.jupiter.api.Test; - -class Tuple3ByteBufTest { - @Test - void testTupleBufferGet() { - ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; - ByteBuf one = allocator.directBuffer(9); - - byte[] bytes = new byte[9]; - ThreadLocalRandom.current().nextBytes(bytes); - one.writeBytes(bytes); - - bytes = new byte[8]; - ThreadLocalRandom.current().nextBytes(bytes); - ByteBuf two = Unpooled.wrappedBuffer(bytes); - - bytes = new byte[9]; - ThreadLocalRandom.current().nextBytes(bytes); - ByteBuf three = Unpooled.wrappedBuffer(bytes); - - ByteBuf tuple = TupleByteBuf.of(one, two, three); - - int anInt = tuple.getInt(16); - - long aLong = tuple.getLong(15); - - short aShort = tuple.getShort(8); - - int medium = tuple.getMedium(8); - } - - @Test - void testTuple3BufferSlicing() { - ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; - ByteBuf one = allocator.directBuffer(); - ByteBufUtil.writeUtf8(one, "foo"); - - ByteBuf two = allocator.directBuffer(); - ByteBufUtil.writeUtf8(two, "bar"); - - ByteBuf three = allocator.directBuffer(); - ByteBufUtil.writeUtf8(three, "bar"); - - ByteBuf buf = TupleByteBuf.of(one, two, three); - - String s = buf.slice(0, 6).toString(Charset.defaultCharset()); - Assert.assertEquals("foobar", s); - - String s1 = buf.slice(3, 6).toString(Charset.defaultCharset()); - Assert.assertEquals("barbar", s1); - - String s2 = buf.slice(4, 4).toString(Charset.defaultCharset()); - Assert.assertEquals("arba", s2); - } - - @Test - void testTuple3ToNioBuffers() throws Exception { - ByteBufAllocator allocator = ByteBufAllocator.DEFAULT; - ByteBuf one = allocator.directBuffer(); - ByteBufUtil.writeUtf8(one, "one"); - - ByteBuf two = allocator.directBuffer(); - ByteBufUtil.writeUtf8(two, "two"); - - ByteBuf three = allocator.directBuffer(); - ByteBufUtil.writeUtf8(three, "three"); - - ByteBuf buf = TupleByteBuf.of(one, two, three); - ByteBuffer[] byteBuffers = buf.nioBuffers(); - - Assert.assertEquals(3, byteBuffers.length); - - ByteBuffer bb = byteBuffers[0]; - byte[] dst = new byte[bb.remaining()]; - bb.get(dst); - Assert.assertEquals("one", new String(dst, "UTF-8")); - - bb = byteBuffers[1]; - dst = new byte[bb.remaining()]; - bb.get(dst); - Assert.assertEquals("two", new String(dst, "UTF-8")); - - bb = byteBuffers[2]; - dst = new byte[bb.remaining()]; - bb.get(dst); - Assert.assertEquals("three", new String(dst, "UTF-8")); - } -} From d3dc85f32ec12e553c382dda28e8c3ba114f95aa Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Thu, 30 Apr 2020 08:52:52 +0100 Subject: [PATCH 161/181] Deprecate AbstractRSocket and provide alternative (#805) --- .../main/java/io/rsocket/AbstractRSocket.java | 35 +-------- .../src/main/java/io/rsocket/RSocket.java | 37 +++++++-- .../main/java/io/rsocket/SocketAcceptor.java | 54 ++++++++++++- .../io/rsocket/core/RSocketConnector.java | 3 +- .../java/io/rsocket/core/RSocketServer.java | 3 +- .../io/rsocket/core/RSocketResponderTest.java | 27 ++++--- .../java/io/rsocket/core/RSocketTest.java | 13 ++-- .../tcp/channel/ChannelEchoClient.java | 31 +++----- .../transport/tcp/duplex/DuplexClient.java | 23 +++--- .../transport/tcp/lease/LeaseExample.java | 19 ++--- .../tcp/requestresponse/HelloWorldClient.java | 33 ++++---- .../tcp/resume/ResumeFileTransfer.java | 50 +++++-------- .../transport/tcp/stream/StreamingClient.java | 23 ++---- .../transport/ws/WebSocketHeadersSample.java | 43 +++++------ .../rsocket/integration/IntegrationTest.java | 5 +- .../integration/InteractionsLoadTest.java | 8 +- .../integration/TcpIntegrationTest.java | 13 ++-- .../rsocket/integration/TestingStreaming.java | 75 ++++++------------- .../rsocket/resume/ResumeIntegrationTest.java | 8 +- .../client/LoadBalancedRSocketMono.java | 18 ++++- .../java/io/rsocket/test/PingHandler.java | 3 +- .../java/io/rsocket/test/TestRSocket.java | 4 +- .../io/rsocket/integration/FragmentTest.java | 9 +-- .../WebSocketTransportIntegrationTest.java | 17 +---- .../WebsocketPingPongIntegrationTest.java | 12 +-- 25 files changed, 264 insertions(+), 302 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/AbstractRSocket.java b/rsocket-core/src/main/java/io/rsocket/AbstractRSocket.java index c099a3120..7f39956dc 100644 --- a/rsocket-core/src/main/java/io/rsocket/AbstractRSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/AbstractRSocket.java @@ -16,48 +16,21 @@ package io.rsocket; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; /** * An abstract implementation of {@link RSocket}. All request handling methods emit {@link * UnsupportedOperationException} and hence must be overridden to provide a valid implementation. + * + * @deprecated as of 1.0 in favor of implementing {@link RSocket} directly which has default + * methods. */ +@Deprecated public abstract class AbstractRSocket implements RSocket { private final MonoProcessor onClose = MonoProcessor.create(); - @Override - public Mono fireAndForget(Payload payload) { - payload.release(); - return Mono.error(new UnsupportedOperationException("Fire and forget not implemented.")); - } - - @Override - public Mono requestResponse(Payload payload) { - payload.release(); - return Mono.error(new UnsupportedOperationException("Request-Response not implemented.")); - } - - @Override - public Flux requestStream(Payload payload) { - payload.release(); - return Flux.error(new UnsupportedOperationException("Request-Stream not implemented.")); - } - - @Override - public Flux requestChannel(Publisher payloads) { - return Flux.error(new UnsupportedOperationException("Request-Channel not implemented.")); - } - - @Override - public Mono metadataPush(Payload payload) { - payload.release(); - return Mono.error(new UnsupportedOperationException("Metadata-Push not implemented.")); - } - @Override public void dispose() { onClose.onComplete(); diff --git a/rsocket-core/src/main/java/io/rsocket/RSocket.java b/rsocket-core/src/main/java/io/rsocket/RSocket.java index 5468b4de8..773c93dc2 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocket.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocket.java @@ -33,7 +33,10 @@ public interface RSocket extends Availability, Closeable { * @return {@code Publisher} that completes when the passed {@code payload} is successfully * handled, otherwise errors. */ - Mono fireAndForget(Payload payload); + default Mono fireAndForget(Payload payload) { + payload.release(); + return Mono.error(new UnsupportedOperationException("Fire-and-Forget not implemented.")); + } /** * Request-Response interaction model of {@code RSocket}. @@ -42,7 +45,10 @@ public interface RSocket extends Availability, Closeable { * @return {@code Publisher} containing at most a single {@code Payload} representing the * response. */ - Mono requestResponse(Payload payload); + default Mono requestResponse(Payload payload) { + payload.release(); + return Mono.error(new UnsupportedOperationException("Request-Response not implemented.")); + } /** * Request-Stream interaction model of {@code RSocket}. @@ -50,7 +56,10 @@ public interface RSocket extends Availability, Closeable { * @param payload Request payload. * @return {@code Publisher} containing the stream of {@code Payload}s representing the response. */ - Flux requestStream(Payload payload); + default Flux requestStream(Payload payload) { + payload.release(); + return Flux.error(new UnsupportedOperationException("Request-Stream not implemented.")); + } /** * Request-Channel interaction model of {@code RSocket}. @@ -58,7 +67,9 @@ public interface RSocket extends Availability, Closeable { * @param payloads Stream of request payloads. * @return Stream of response payloads. */ - Flux requestChannel(Publisher payloads); + default Flux requestChannel(Publisher payloads) { + return Flux.error(new UnsupportedOperationException("Request-Channel not implemented.")); + } /** * Metadata-Push interaction model of {@code RSocket}. @@ -67,10 +78,26 @@ public interface RSocket extends Availability, Closeable { * @return {@code Publisher} that completes when the passed {@code payload} is successfully * handled, otherwise errors. */ - Mono metadataPush(Payload payload); + default Mono metadataPush(Payload payload) { + payload.release(); + return Mono.error(new UnsupportedOperationException("Metadata-Push not implemented.")); + } @Override default double availability() { return isDisposed() ? 0.0 : 1.0; } + + @Override + default void dispose() {} + + @Override + default boolean isDisposed() { + return false; + } + + @Override + default Mono onClose() { + return Mono.never(); + } } diff --git a/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java b/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java index 85c731eea..a42626e78 100644 --- a/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/SocketAcceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -17,6 +17,9 @@ package io.rsocket; import io.rsocket.exceptions.SetupException; +import java.util.function.Function; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; /** @@ -38,4 +41,53 @@ public interface SocketAcceptor { * @throws SetupException If the acceptor needs to reject the setup of this socket. */ Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket); + + /** Create a {@code SocketAcceptor} that handles requests with the given {@code RSocket}. */ + static SocketAcceptor with(RSocket rsocket) { + return (setup, sendingRSocket) -> Mono.just(rsocket); + } + + /** Create a {@code SocketAcceptor} for fire-and-forget interactions with the given handler. */ + static SocketAcceptor forFireAndForget(Function> handler) { + return with( + new RSocket() { + @Override + public Mono fireAndForget(Payload payload) { + return handler.apply(payload); + } + }); + } + + /** Create a {@code SocketAcceptor} for request-response interactions with the given handler. */ + static SocketAcceptor forRequestResponse(Function> handler) { + return with( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + return handler.apply(payload); + } + }); + } + + /** Create a {@code SocketAcceptor} for request-stream interactions with the given handler. */ + static SocketAcceptor forRequestStream(Function> handler) { + return with( + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + return handler.apply(payload); + } + }); + } + + /** Create a {@code SocketAcceptor} for request-channel interactions with the given handler. */ + static SocketAcceptor forRequestChannel(Function, Flux> handler) { + return with( + new RSocket() { + @Override + public Flux requestChannel(Publisher payloads) { + return handler.apply(payloads); + } + }); + } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 57aebbdf0..a7eed8c76 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -17,7 +17,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; -import io.rsocket.AbstractRSocket; import io.rsocket.ConnectionSetupPayload; import io.rsocket.DuplexConnection; import io.rsocket.Payload; @@ -56,7 +55,7 @@ public class RSocketConnector { private String metadataMimeType = "application/binary"; private String dataMimeType = "application/binary"; - private SocketAcceptor acceptor = (setup, sendingSocket) -> Mono.just(new AbstractRSocket() {}); + private SocketAcceptor acceptor = SocketAcceptor.with(new RSocket() {}); private InitializingInterceptorRegistry interceptors = new InitializingInterceptorRegistry(); private Duration keepAliveInterval = Duration.ofSeconds(20); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index c82a2f40a..19f0c5008 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -17,7 +17,6 @@ package io.rsocket.core; import io.netty.buffer.ByteBuf; -import io.rsocket.AbstractRSocket; import io.rsocket.Closeable; import io.rsocket.ConnectionSetupPayload; import io.rsocket.DuplexConnection; @@ -45,7 +44,7 @@ public final class RSocketServer { private static final String SERVER_TAG = "server"; private static final int MIN_MTU_SIZE = 64; - private SocketAcceptor acceptor = (setup, sendingSocket) -> Mono.just(new AbstractRSocket() {}); + private SocketAcceptor acceptor = SocketAcceptor.with(new RSocket() {}); private InitializingInterceptorRegistry interceptors = new InitializingInterceptorRegistry(); private int mtu = 0; diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index c19456548..2dbf6715b 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -35,7 +35,6 @@ import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; -import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.frame.CancelFrameFlyweight; @@ -164,7 +163,7 @@ public void testCancel() { final int streamId = 4; final AtomicBoolean cancelled = new AtomicBoolean(); rule.setAcceptingSocket( - new AbstractRSocket() { + new RSocket() { @Override public Mono requestResponse(Payload payload) { payload.release(); @@ -193,8 +192,8 @@ public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmen ThreadLocalRandom.current().nextBytes(metadata); ThreadLocalRandom.current().nextBytes(data); final Payload payload = DefaultPayload.create(data, metadata); - final AbstractRSocket acceptingSocket = - new AbstractRSocket() { + final RSocket acceptingSocket = + new RSocket() { @Override public Mono requestResponse(Payload p) { p.release(); @@ -256,7 +255,7 @@ public void checkNoLeaksOnRacingCancelFromRequestChannelAndNextFromUpstream() { AssertSubscriber assertSubscriber = AssertSubscriber.create(); rule.setAcceptingSocket( - new AbstractRSocket() { + new RSocket() { @Override public Flux requestChannel(Publisher payloads) { payloads.subscribe(assertSubscriber); @@ -312,7 +311,7 @@ public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChann FluxSink[] sinks = new FluxSink[1]; rule.setAcceptingSocket( - new AbstractRSocket() { + new RSocket() { @Override public Flux requestChannel(Publisher payloads) { ((Flux) payloads) @@ -352,7 +351,7 @@ public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestChann FluxSink[] sinks = new FluxSink[1]; rule.setAcceptingSocket( - new AbstractRSocket() { + new RSocket() { @Override public Flux requestChannel(Publisher payloads) { ((Flux) payloads) @@ -397,7 +396,7 @@ public Flux requestChannel(Publisher payloads) { FluxSink[] sinks = new FluxSink[1]; AssertSubscriber assertSubscriber = AssertSubscriber.create(); rule.setAcceptingSocket( - new AbstractRSocket() { + new RSocket() { @Override public Flux requestChannel(Publisher payloads) { payloads.subscribe(assertSubscriber); @@ -466,7 +465,7 @@ public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestStrea FluxSink[] sinks = new FluxSink[1]; rule.setAcceptingSocket( - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { payload.release(); @@ -503,7 +502,7 @@ public void checkNoLeaksOnRacingBetweenDownstreamCancelAndOnNextFromRequestRespo Operators.MonoSubscriber[] sources = new Operators.MonoSubscriber[1]; rule.setAcceptingSocket( - new AbstractRSocket() { + new RSocket() { @Override public Mono requestResponse(Payload payload) { payload.release(); @@ -540,7 +539,7 @@ public void simpleDiscardRequestStreamTest() { FluxSink[] sinks = new FluxSink[1]; rule.setAcceptingSocket( - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { payload.release(); @@ -569,7 +568,7 @@ public void simpleDiscardRequestChannelTest() { ByteBufAllocator allocator = rule.alloc(); rule.setAcceptingSocket( - new AbstractRSocket() { + new RSocket() { @Override public Flux requestChannel(Publisher payloads) { return (Flux) payloads; @@ -619,7 +618,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( TestPublisher testPublisher = TestPublisher.create(); rule.setAcceptingSocket( - new AbstractRSocket() { + new RSocket() { @Override public Mono fireAndForget(Payload payload) { Mono.just(payload).subscribe(assertSubscriber); @@ -720,7 +719,7 @@ public static class ServerSocketRule extends AbstractSocketRule requestResponse(Payload payload) { return Mono.just(payload); diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index 4a2c43ef8..02c3dfca8 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -23,7 +23,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; @@ -83,7 +82,7 @@ public void testRequestReplyNoError() { @Test(timeout = 2000) public void testHandlerEmitsError() { rule.setRequestAcceptor( - new AbstractRSocket() { + new RSocket() { @Override public Mono requestResponse(Payload payload) { return Mono.error(new NullPointerException("Deliberate exception.")); @@ -102,7 +101,7 @@ public Mono requestResponse(Payload payload) { @Test(timeout = 2000) public void testHandlerEmitsCustomError() { rule.setRequestAcceptor( - new AbstractRSocket() { + new RSocket() { @Override public Mono requestResponse(Payload payload) { return Mono.error( @@ -129,7 +128,7 @@ public Mono requestResponse(Payload payload) { @Test(timeout = 2000) public void testRequestPropagatesCorrectlyForRequestChannel() { rule.setRequestAcceptor( - new AbstractRSocket() { + new RSocket() { @Override public Flux requestChannel(Publisher payloads) { return Flux.from(payloads) @@ -170,7 +169,7 @@ public void testChannel() throws Exception { public void testErrorPropagatesCorrectly() { AtomicReference error = new AtomicReference<>(); rule.setRequestAcceptor( - new AbstractRSocket() { + new RSocket() { @Override public Flux requestChannel(Publisher payloads) { return Flux.from(payloads).doOnError(error::set); @@ -291,7 +290,7 @@ void initRequestChannelCase( TestPublisher responderPublisher, AssertSubscriber responderSubscriber) { rule.setRequestAcceptor( - new AbstractRSocket() { + new RSocket() { @Override public Flux requestChannel(Publisher payloads) { payloads.subscribe(responderSubscriber); @@ -446,7 +445,7 @@ protected void init() { requestAcceptor = null != requestAcceptor ? requestAcceptor - : new AbstractRSocket() { + : new RSocket() { @Override public Mono requestResponse(Payload payload) { return Mono.just(payload); 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 71e48790f..b532c0140 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 @@ -16,8 +16,6 @@ package io.rsocket.examples.transport.tcp.channel; -import io.rsocket.AbstractRSocket; -import io.rsocket.ConnectionSetupPayload; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; @@ -27,18 +25,25 @@ import io.rsocket.transport.netty.server.TcpServerTransport; 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; -import reactor.core.publisher.Mono; public final class ChannelEchoClient { private static final Logger logger = LoggerFactory.getLogger(ChannelEchoClient.class); public static void main(String[] args) { - RSocketServer.create(new EchoAcceptor()) + + SocketAcceptor echoAcceptor = + SocketAcceptor.forRequestChannel( + payloads -> + Flux.from(payloads) + .map(Payload::getDataUtf8) + .map(s -> "Echo: " + s) + .map(DefaultPayload::create)); + + RSocketServer.create(echoAcceptor) .bind(TcpServerTransport.create("localhost", 7000)) .subscribe(); @@ -55,20 +60,4 @@ public static void main(String[] args) { .then() .block(); } - - private static class EchoAcceptor implements SocketAcceptor { - @Override - public Mono accept(ConnectionSetupPayload setupPayload, RSocket reactiveSocket) { - return Mono.just( - new AbstractRSocket() { - @Override - public Flux requestChannel(Publisher payloads) { - return Flux.from(payloads) - .map(Payload::getDataUtf8) - .map(s -> "Echo: " + s) - .map(DefaultPayload::create); - } - }); - } - } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/duplex/DuplexClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/duplex/DuplexClient.java index bfa58bf40..3eba5a800 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/duplex/DuplexClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/duplex/DuplexClient.java @@ -16,7 +16,8 @@ package io.rsocket.examples.transport.tcp.duplex; -import io.rsocket.AbstractRSocket; +import static io.rsocket.SocketAcceptor.forRequestStream; + import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.core.RSocketConnector; @@ -31,6 +32,7 @@ public final class DuplexClient { public static void main(String[] args) { + RSocketServer.create( (setup, rsocket) -> { rsocket @@ -39,26 +41,21 @@ public static void main(String[] args) { .log() .subscribe(); - return Mono.just(new AbstractRSocket() {}); + return Mono.just(new RSocket() {}); }) .bind(TcpServerTransport.create("localhost", 7000)) .subscribe(); - RSocket socket = + RSocket rsocket = RSocketConnector.create() .acceptor( - (setup, rsocket) -> - Mono.just( - new AbstractRSocket() { - @Override - public Flux requestStream(Payload payload) { - return Flux.interval(Duration.ofSeconds(1)) - .map(aLong -> DefaultPayload.create("Bi-di Response => " + aLong)); - } - })) + forRequestStream( + payload -> + Flux.interval(Duration.ofSeconds(1)) + .map(aLong -> DefaultPayload.create("Bi-di Response => " + aLong)))) .connect(TcpClientTransport.create("localhost", 7000)) .block(); - socket.onClose().block(); + rsocket.onClose().block(); } } diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java index a12c9a170..3eaebd89a 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/lease/LeaseExample.java @@ -18,9 +18,9 @@ import static java.time.Duration.ofSeconds; -import io.rsocket.AbstractRSocket; 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.lease.Lease; @@ -45,7 +45,7 @@ public static void main(String[] args) { CloseableChannel server = RSocketServer.create( - (setup, sendingRSocket) -> Mono.just(new ServerAcceptor(sendingRSocket))) + (setup, sendingRSocket) -> Mono.just(new ServerRSocket(sendingRSocket))) .lease( () -> Leases.create() @@ -62,7 +62,9 @@ public static void main(String[] args) { Leases.create() .sender(new LeaseSender(CLIENT_TAG, 3_000, 5)) .receiver(new LeaseReceiver(CLIENT_TAG))) - .acceptor((rSocket, setup) -> Mono.just(new ClientAcceptor())) + .acceptor( + SocketAcceptor.forRequestResponse( + payload -> Mono.just(DefaultPayload.create("Client Response " + new Date())))) .connect(TcpClientTransport.create(server.address())) .block(); @@ -133,17 +135,10 @@ private static class NoopStats implements LeaseStats { public void onEvent(EventType eventType) {} } - private static class ClientAcceptor extends AbstractRSocket { - @Override - public Mono requestResponse(Payload payload) { - return Mono.just(DefaultPayload.create("Client Response " + new Date())); - } - } - - private static class ServerAcceptor extends AbstractRSocket { + private static class ServerRSocket implements RSocket { private final RSocket senderRSocket; - public ServerAcceptor(RSocket senderRSocket) { + public ServerRSocket(RSocket senderRSocket) { this.senderRSocket = senderRSocket; } 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 1b9994c2f..85faeee82 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 @@ -16,9 +16,9 @@ package io.rsocket.examples.transport.tcp.requestresponse; -import io.rsocket.AbstractRSocket; 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.transport.netty.client.TcpClientTransport; @@ -33,22 +33,23 @@ public final class HelloWorldClient { private static final Logger logger = LoggerFactory.getLogger(HelloWorldClient.class); public static void main(String[] args) { - RSocketServer.create( - (setupPayload, reactiveSocket) -> - Mono.just( - new AbstractRSocket() { - 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); - } - } - })) + 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)) .bind(TcpServerTransport.create("localhost", 7000)) .subscribe(); 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 d449dd205..93b54e146 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 @@ -16,9 +16,9 @@ package io.rsocket.examples.transport.tcp.resume; -import io.rsocket.AbstractRSocket; 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; @@ -30,7 +30,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import reactor.util.retry.Retry; public class ResumeFileTransfer { @@ -40,18 +39,31 @@ public class ResumeFileTransfer { private static final Logger logger = LoggerFactory.getLogger(ResumeFileTransfer.class); public static void main(String[] args) { - RequestCodec requestCodec = new RequestCodec(); + Resume resume = new Resume() .sessionDuration(Duration.ofMinutes(5)) .retry( Retry.fixedDelay(Long.MAX_VALUE, Duration.ofSeconds(1)) - .doBeforeRetry( - retrySignal -> - logger.debug("Disconnected. Trying to resume connection..."))); + .doBeforeRetry(s -> logger.debug("Disconnected. Trying to resume..."))); + + RequestCodec codec = new RequestCodec(); CloseableChannel server = - RSocketServer.create((setup, rSocket) -> Mono.just(new FileServer(requestCodec))) + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> { + Request request = codec.decode(payload); + payload.release(); + String fileName = request.getFileName(); + int chunkSize = request.getChunkSize(); + + Flux ticks = Flux.interval(Duration.ofMillis(500)).onBackpressureDrop(); + + return Files.fileSource(fileName, chunkSize) + .map(DefaultPayload::create) + .zipWith(ticks, (p, tick) -> p); + })) .resume(resume) .bind(TcpServerTransport.create("localhost", 8000)) .block(); @@ -63,35 +75,13 @@ public static void main(String[] args) { .block(); client - .requestStream(requestCodec.encode(new Request(16, "lorem.txt"))) + .requestStream(codec.encode(new Request(16, "lorem.txt"))) .doFinally(s -> server.dispose()) .subscribe(Files.fileSink("rsocket-examples/out/lorem_output.txt", PREFETCH_WINDOW_SIZE)); server.onClose().block(); } - private static class FileServer extends AbstractRSocket { - private final RequestCodec requestCodec; - - public FileServer(RequestCodec requestCodec) { - this.requestCodec = requestCodec; - } - - @Override - public Flux requestStream(Payload payload) { - Request request = requestCodec.decode(payload); - payload.release(); - String fileName = request.getFileName(); - int chunkSize = request.getChunkSize(); - - Flux ticks = Flux.interval(Duration.ofMillis(500)).onBackpressureDrop(); - - return Files.fileSource(fileName, chunkSize) - .map(DefaultPayload::create) - .zipWith(ticks, (p, tick) -> p); - } - } - private static class RequestCodec { public Payload encode(Request request) { diff --git a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java index 1ef2b7a90..6ac329d56 100644 --- a/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/stream/StreamingClient.java @@ -16,8 +16,6 @@ package io.rsocket.examples.transport.tcp.stream; -import io.rsocket.AbstractRSocket; -import io.rsocket.ConnectionSetupPayload; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; @@ -30,14 +28,17 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; public final class StreamingClient { private static final Logger logger = LoggerFactory.getLogger(StreamingClient.class); public static void main(String[] args) { - RSocketServer.create(new SocketAcceptorImpl()) + RSocketServer.create( + SocketAcceptor.forRequestStream( + payload -> + Flux.interval(Duration.ofMillis(100)) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)))) .bind(TcpServerTransport.create("localhost", 7000)) .subscribe(); @@ -54,18 +55,4 @@ public static void main(String[] args) { .then() .block(); } - - private static class SocketAcceptorImpl implements SocketAcceptor { - @Override - public Mono accept(ConnectionSetupPayload setupPayload, RSocket reactiveSocket) { - return Mono.just( - new AbstractRSocket() { - @Override - public Flux requestStream(Payload payload) { - return Flux.interval(Duration.ofMillis(100)) - .map(aLong -> DefaultPayload.create("Interval: " + aLong)); - } - }); - } - } } 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 24f029845..2ab73116d 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 @@ -17,8 +17,6 @@ package io.rsocket.examples.transport.ws; import io.netty.handler.codec.http.HttpResponseStatus; -import io.rsocket.AbstractRSocket; -import io.rsocket.ConnectionSetupPayload; import io.rsocket.DuplexConnection; import io.rsocket.Payload; import io.rsocket.RSocket; @@ -47,9 +45,8 @@ public class WebSocketHeadersSample { public static void main(String[] args) { ServerTransport.ConnectionAcceptor acceptor = - RSocketServer.create() + RSocketServer.create(SocketAcceptor.with(new ServerRSocket())) .payloadDecoder(PayloadDecoder.ZERO_COPY) - .acceptor(new SocketAcceptorImpl()) .asConnectionAcceptor(); DisposableServer disposableServer = @@ -114,29 +111,23 @@ public static void main(String[] args) { rSocket.requestResponse(payload1).block(); } - private static class SocketAcceptorImpl implements SocketAcceptor { + private static class ServerRSocket implements RSocket { + + @Override + public Mono fireAndForget(Payload payload) { + // System.out.println(payload.getDataUtf8()); + payload.release(); + return Mono.empty(); + } + + @Override + public Mono requestResponse(Payload payload) { + return Mono.just(payload); + } + @Override - public Mono accept(ConnectionSetupPayload setupPayload, RSocket reactiveSocket) { - return Mono.just( - new AbstractRSocket() { - - @Override - public Mono fireAndForget(Payload payload) { - // System.out.println(payload.getDataUtf8()); - payload.release(); - return Mono.empty(); - } - - @Override - public Mono requestResponse(Payload payload) { - return Mono.just(payload); - } - - @Override - public Flux requestChannel(Publisher payloads) { - return Flux.from(payloads).subscribeOn(Schedulers.single()); - } - }); + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads).subscribeOn(Schedulers.single()); } } } diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java index 1ef7771cd..e2471f2fc 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/IntegrationTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -23,7 +23,6 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.core.RSocketConnector; @@ -124,7 +123,7 @@ public void startup() { .subscribe(); return Mono.just( - new AbstractRSocket() { + new RSocket() { @Override public Mono requestResponse(Payload payload) { return Mono.just(DefaultPayload.create("RESPONSE", "METADATA")) diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java index d24083ea6..48e5baaa7 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/InteractionsLoadTest.java @@ -1,8 +1,8 @@ package io.rsocket.integration; -import io.rsocket.AbstractRSocket; 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.test.SlowTest; @@ -15,7 +15,6 @@ import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; public class InteractionsLoadTest { @@ -23,7 +22,7 @@ public class InteractionsLoadTest { @SlowTest public void channel() { CloseableChannel server = - RSocketServer.create((setup, rsocket) -> Mono.just(new EchoRSocket())) + RSocketServer.create(SocketAcceptor.with(new EchoRSocket())) .bind(TcpServerTransport.create("localhost", 0)) .block(Duration.ofSeconds(10)); @@ -66,7 +65,8 @@ private static Flux input() { return interval; } - private static class EchoRSocket extends AbstractRSocket { + private static class EchoRSocket implements RSocket { + @Override public Flux requestChannel(Publisher payloads) { return Flux.from(payloads) diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java index 7133820ca..de27bcb9b 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TcpIntegrationTest.java @@ -19,7 +19,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.core.RSocketConnector; @@ -41,7 +40,7 @@ import reactor.core.scheduler.Schedulers; public class TcpIntegrationTest { - private AbstractRSocket handler; + private RSocket handler; private CloseableChannel server; @@ -65,7 +64,7 @@ public void cleanup() { @Test(timeout = 15_000L) public void testCompleteWithoutNext() { handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { return Flux.empty(); @@ -81,7 +80,7 @@ public Flux requestStream(Payload payload) { @Test(timeout = 15_000L) public void testSingleStream() { handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { return Flux.just(DefaultPayload.create("RESPONSE", "METADATA")); @@ -98,7 +97,7 @@ public Flux requestStream(Payload payload) { @Test(timeout = 15_000L) public void testZeroPayload() { handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { return Flux.just(EmptyPayload.INSTANCE); @@ -115,7 +114,7 @@ public Flux requestStream(Payload payload) { @Test(timeout = 15_000L) public void testRequestResponseErrors() { handler = - new AbstractRSocket() { + new RSocket() { boolean first = true; @Override @@ -155,7 +154,7 @@ public void testTwoConcurrentStreams() throws InterruptedException { map.put("REQUEST2", processor2); handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { return map.get(payload.getDataUtf8()); diff --git a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java index 8fe09430a..7d34ba478 100644 --- a/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java +++ b/rsocket-examples/src/test/java/io/rsocket/integration/TestingStreaming.java @@ -16,9 +16,9 @@ package io.rsocket.integration; -import io.rsocket.AbstractRSocket; import io.rsocket.Closeable; import io.rsocket.Payload; +import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; import io.rsocket.exceptions.ApplicationErrorException; @@ -29,7 +29,6 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.Test; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; public class TestingStreaming { LocalServerTransport serverTransport = LocalServerTransport.create("test"); @@ -40,27 +39,17 @@ public void testRangeButThrowException() { try { server = RSocketServer.create( - (connectionSetupPayload, rSocket) -> - Mono.just( - new AbstractRSocket() { - @Override - public double availability() { - return 1.0; - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.range(1, 1000) - .doOnNext( - i -> { - if (i > 3) { - throw new RuntimeException("BOOM!"); - } - }) - .map(l -> DefaultPayload.create("l -> " + l)) - .cast(Payload.class); - } - })) + SocketAcceptor.forRequestStream( + payload -> + Flux.range(1, 1000) + .doOnNext( + i -> { + if (i > 3) { + throw new RuntimeException("BOOM!"); + } + }) + .map(l -> DefaultPayload.create("l -> " + l)) + .cast(Payload.class))) .bind(serverTransport) .block(); @@ -78,21 +67,11 @@ public void testRangeOfConsumers() { try { server = RSocketServer.create( - (connectionSetupPayload, rSocket) -> - Mono.just( - new AbstractRSocket() { - @Override - public double availability() { - return 1.0; - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.range(1, 1000) - .map(l -> DefaultPayload.create("l -> " + l)) - .cast(Payload.class); - } - })) + SocketAcceptor.forRequestStream( + payload -> + Flux.range(1, 1000) + .map(l -> DefaultPayload.create("l -> " + l)) + .cast(Payload.class))) .bind(serverTransport) .block(); @@ -121,21 +100,11 @@ public void testSingleConsumer() { try { server = RSocketServer.create( - (connectionSetupPayload, rSocket) -> - Mono.just( - new AbstractRSocket() { - @Override - public double availability() { - return 1.0; - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.range(1, 10_000) - .map(l -> DefaultPayload.create("l -> " + l)) - .cast(Payload.class); - } - })) + SocketAcceptor.forRequestStream( + payload -> + Flux.range(1, 10_000) + .map(l -> DefaultPayload.create("l -> " + l)) + .cast(Payload.class))) .bind(serverTransport) .block(); diff --git a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java index bd2db39c7..b2dad0022 100644 --- a/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java +++ b/rsocket-examples/src/test/java/io/rsocket/resume/ResumeIntegrationTest.java @@ -16,9 +16,9 @@ package io.rsocket.resume; -import io.rsocket.AbstractRSocket; 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; @@ -127,7 +127,7 @@ public void reconnectOnMissingSession() { @Test void serverMissingResume() { CloseableChannel closeableChannel = - RSocketServer.create((setupPayload, rSocket) -> Mono.just(new TestResponderRSocket())) + RSocketServer.create(SocketAcceptor.with(new TestResponderRSocket())) .bind(serverTransport(SERVER_HOST, SERVER_PORT)) .block(); @@ -194,7 +194,7 @@ private static Mono newServerRSocket() { } private static Mono newServerRSocket(int sessionDurationSeconds) { - return RSocketServer.create((setup, rsocket) -> Mono.just(new TestResponderRSocket())) + return RSocketServer.create(SocketAcceptor.with(new TestResponderRSocket())) .resume( new Resume() .sessionDuration(Duration.ofSeconds(sessionDurationSeconds)) @@ -203,7 +203,7 @@ private static Mono newServerRSocket(int sessionDurationSecond .bind(serverTransport(SERVER_HOST, SERVER_PORT)); } - private static class TestResponderRSocket extends AbstractRSocket { + private static class TestResponderRSocket implements RSocket { AtomicInteger counter = new AtomicInteger(); diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java index ed7550233..65ce80934 100644 --- a/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/LoadBalancedRSocketMono.java @@ -536,7 +536,7 @@ public Mono onClose() { * Wrapper of a RSocket, it computes statistics about the req/resp calls and update availability * accordingly. */ - private class WeightedSocket extends AbstractRSocket implements LoadBalancerSocketMetrics { + private class WeightedSocket implements LoadBalancerSocketMetrics, RSocket { private static final double STARTUP_PENALTY = Long.MAX_VALUE >> 12; private final Quantile lowerQuantile; @@ -554,6 +554,7 @@ private class WeightedSocket extends AbstractRSocket implements LoadBalancerSock private AtomicLong pendingStreams; // number of active streams private volatile double availability = 0.0; + private final MonoProcessor onClose = MonoProcessor.create(); WeightedSocket( RSocketSupplier factory, @@ -791,6 +792,21 @@ public double availability() { return availability; } + @Override + public void dispose() { + onClose.onComplete(); + } + + @Override + public boolean isDisposed() { + return onClose.isDisposed(); + } + + @Override + public Mono onClose() { + return onClose; + } + @Override public String toString() { return "WeightedSocket(" diff --git a/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java b/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java index 902014e7f..47f40a59d 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java +++ b/rsocket-test/src/main/java/io/rsocket/test/PingHandler.java @@ -16,7 +16,6 @@ package io.rsocket.test; -import io.rsocket.AbstractRSocket; import io.rsocket.ConnectionSetupPayload; import io.rsocket.Payload; import io.rsocket.RSocket; @@ -43,7 +42,7 @@ public PingHandler(byte[] data) { @Override public Mono accept(ConnectionSetupPayload setup, RSocket sendingSocket) { return Mono.just( - new AbstractRSocket() { + new RSocket() { @Override public Mono requestResponse(Payload payload) { payload.release(); diff --git a/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java b/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java index 26163d3a6..d48700445 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TestRSocket.java @@ -16,14 +16,14 @@ package io.rsocket.test; -import io.rsocket.AbstractRSocket; import io.rsocket.Payload; +import io.rsocket.RSocket; import io.rsocket.util.DefaultPayload; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -public class TestRSocket extends AbstractRSocket { +public class TestRSocket implements RSocket { private final String data; private final String metadata; diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java index 0ea938af2..23041ec65 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/integration/FragmentTest.java @@ -18,7 +18,6 @@ import static org.assertj.core.api.Assertions.assertThat; -import io.rsocket.AbstractRSocket; import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.core.RSocketConnector; @@ -38,7 +37,7 @@ import reactor.core.publisher.Mono; public class FragmentTest { - private AbstractRSocket handler; + private RSocket handler; private CloseableChannel server; private String message = null; private String metaData = null; @@ -89,7 +88,7 @@ void testFragmentNoMetaData(int clientFrameSize, int serverFrameSize) { System.out.println( "-------------------------------------------------testFragmentNoMetaData-------------------------------------------------"); handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { String request = payload.getDataUtf8(); @@ -119,7 +118,7 @@ void testFragmentRequestMetaDataOnly(int clientFrameSize, int serverFrameSize) { System.out.println( "-------------------------------------------------testFragmentRequestMetaDataOnly-------------------------------------------------"); handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { String request = payload.getDataUtf8(); @@ -150,7 +149,7 @@ void testFragmentBothMetaData(int clientFrameSize, int serverFrameSize) { System.out.println( "-------------------------------------------------testFragmentBothMetaData-------------------------------------------------"); handler = - new AbstractRSocket() { + new RSocket() { @Override public Flux requestStream(Payload payload) { String request = payload.getDataUtf8(); diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java index 7028a3846..c418dea0f 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebSocketTransportIntegrationTest.java @@ -1,8 +1,7 @@ package io.rsocket.transport.netty; -import io.rsocket.AbstractRSocket; -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.transport.ServerTransport; @@ -14,7 +13,6 @@ import java.time.Duration; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; import reactor.netty.DisposableServer; import reactor.netty.http.server.HttpServer; import reactor.test.StepVerifier; @@ -25,16 +23,9 @@ public class WebSocketTransportIntegrationTest { public void sendStreamOfDataWithExternalHttpServerTest() { ServerTransport.ConnectionAcceptor acceptor = RSocketServer.create( - (setupPayload, sendingRSocket) -> { - return Mono.just( - new AbstractRSocket() { - @Override - public Flux requestStream(Payload payload) { - return Flux.range(0, 10) - .map(i -> DefaultPayload.create(String.valueOf(i))); - } - }); - }) + SocketAcceptor.forRequestStream( + payload -> + Flux.range(0, 10).map(i -> DefaultPayload.create(String.valueOf(i))))) .asConnectionAcceptor(); DisposableServer server = diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java index ab6c343de..e2ee9e521 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketPingPongIntegrationTest.java @@ -8,10 +8,9 @@ import io.netty.handler.codec.http.websocketx.PongWebSocketFrame; import io.netty.handler.codec.http.websocketx.WebSocketFrame; import io.netty.util.ReferenceCountUtil; -import io.rsocket.AbstractRSocket; import io.rsocket.Closeable; -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.transport.ServerTransport; @@ -47,7 +46,7 @@ void tearDown() { @MethodSource("provideServerTransport") void webSocketPingPong(ServerTransport serverTransport) { server = - RSocketServer.create((setup, sendingSocket) -> Mono.just(new EchoRSocket())) + RSocketServer.create(SocketAcceptor.forRequestResponse(Mono::just)) .bind(serverTransport) .block(); @@ -100,13 +99,6 @@ private static Stream provideServerTransport() { HttpServer.create().host(host).port(port), routes -> {}, "/"))); } - private static class EchoRSocket extends AbstractRSocket { - @Override - public Mono requestResponse(Payload payload) { - return Mono.just(payload); - } - } - private static class PingSender extends ChannelInboundHandlerAdapter { private final MonoProcessor channel = MonoProcessor.create(); private final MonoProcessor pong = MonoProcessor.create(); From 31880691d2e582d13f39a576c2b473f95a975b66 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 30 Apr 2020 16:09:49 +0300 Subject: [PATCH 162/181] reduces maintenance complexity (#806) --- .../io/rsocket/core/RSocketRequester.java | 216 +- .../java/io/rsocket/internal/BitUtil.java | 287 --- .../io/rsocket/internal/CollectionUtil.java | 121 -- .../java/io/rsocket/internal/Hashing.java | 124 -- .../internal/LimitableRequestPublisher.java | 204 -- .../rsocket/internal/SwitchTransformFlux.java | 581 ------ .../io/rsocket/internal/UnicastMonoEmpty.java | 84 - .../internal/UnicastMonoProcessor.java | 509 ----- .../java/io/rsocket/util/DisposableUtils.java | 45 - .../rsocket/util/DuplexConnectionProxy.java | 71 - .../main/java/io/rsocket/util/Function3.java | 22 - .../io/rsocket/util/MonoLifecycleHandler.java | 21 - .../rsocket/util/MultiSubscriberRSocket.java | 54 - .../java/io/rsocket/util/OnceConsumer.java | 33 - .../java/io/rsocket/util/RecyclerFactory.java | 46 - .../core/RSocketRequesterSubscribersTest.java | 18 - .../io/rsocket/core/RSocketRequesterTest.java | 18 - .../LimitableRequestPublisherTest.java | 33 - .../internal/SwitchTransformFluxTest.java | 446 ----- .../internal/UnicastMonoEmptyTest.java | 97 - .../internal/UnicastMonoProcessorTest.java | 1780 ----------------- 21 files changed, 122 insertions(+), 4688 deletions(-) delete mode 100644 rsocket-core/src/main/java/io/rsocket/internal/BitUtil.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/internal/CollectionUtil.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/internal/Hashing.java delete mode 100755 rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/internal/SwitchTransformFlux.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoEmpty.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/util/DisposableUtils.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/util/DuplexConnectionProxy.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/util/Function3.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/util/MonoLifecycleHandler.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/util/MultiSubscriberRSocket.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/util/OnceConsumer.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/util/RecyclerFactory.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/internal/LimitableRequestPublisherTest.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/internal/SwitchTransformFluxTest.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoEmptyTest.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index fefb06003..f762bfe99 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -45,21 +45,18 @@ import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.SynchronizedIntObjectHashMap; import io.rsocket.internal.UnboundedProcessor; -import io.rsocket.internal.UnicastMonoEmpty; -import io.rsocket.internal.UnicastMonoProcessor; import io.rsocket.keepalive.KeepAliveFramesAcceptor; import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.keepalive.KeepAliveSupport; import io.rsocket.lease.RequesterLeaseHandler; -import io.rsocket.util.MonoLifecycleHandler; import java.nio.channels.ClosedChannelException; import java.util.concurrent.CancellationException; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; import java.util.function.LongConsumer; import java.util.function.Supplier; -import javax.annotation.Nonnull; import javax.annotation.Nullable; import org.reactivestreams.Processor; import org.reactivestreams.Publisher; @@ -210,15 +207,25 @@ private Mono handleFireAndForget(Payload payload) { return Mono.error(new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); } + final AtomicBoolean once = new AtomicBoolean(); final int streamId = streamIdSupplier.nextStreamId(receivers); - return UnicastMonoEmpty.newInstance( + return Mono.defer( () -> { - ByteBuf requestFrame = - RequestFireAndForgetFrameFlyweight.encodeReleasingPayload( - allocator, streamId, payload); + if (once.getAndSet(true)) { + return Mono.error( + new IllegalStateException("FireAndForgetMono allows only a single subscriber")); + } + + return Mono.empty() + .doOnSubscribe( + (__) -> { + ByteBuf requestFrame = + RequestFireAndForgetFrameFlyweight.encodeReleasingPayload( + allocator, streamId, payload); - sendProcessor.onNext(requestFrame); + sendProcessor.onNext(requestFrame); + }); }); } @@ -236,34 +243,37 @@ private Mono handleRequestResponse(final Payload payload) { int streamId = streamIdSupplier.nextStreamId(receivers); final UnboundedProcessor sendProcessor = this.sendProcessor; - - UnicastMonoProcessor receiver = - UnicastMonoProcessor.create( - new MonoLifecycleHandler() { - @Override - public void doOnSubscribe() { - final ByteBuf requestFrame = - RequestResponseFrameFlyweight.encodeReleasingPayload( - allocator, streamId, payload); - - sendProcessor.onNext(requestFrame); - } - - @Override - public void doOnTerminal( - @Nonnull SignalType signalType, - @Nullable Payload element, - @Nullable Throwable e) { - if (signalType == SignalType.CANCEL) { - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - } - removeStreamReceiver(streamId); - } - }); + final UnicastProcessor receiver = UnicastProcessor.create(Queues.one().get()); + final AtomicBoolean once = new AtomicBoolean(); receivers.put(streamId, receiver); - return receiver.doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); + return Mono.defer( + () -> { + if (once.getAndSet(true)) { + return Mono.error( + new IllegalStateException("RequestResponseMono allows only a single subscriber")); + } + + return receiver + .next() + .doOnSubscribe( + (__) -> { + ByteBuf requestFrame = + RequestResponseFrameFlyweight.encodeReleasingPayload( + allocator, streamId, payload); + + sendProcessor.onNext(requestFrame); + }) + .doFinally( + signalType -> { + if (signalType == SignalType.CANCEL) { + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + } + removeStreamReceiver(streamId); + }) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); + }); } private Flux handleRequestStream(final Payload payload) { @@ -283,65 +293,76 @@ private Flux handleRequestStream(final Payload payload) { final UnboundedProcessor sendProcessor = this.sendProcessor; final UnicastProcessor receiver = UnicastProcessor.create(); final AtomicInteger wip = new AtomicInteger(0); + final AtomicBoolean once = new AtomicBoolean(); receivers.put(streamId, receiver); - return receiver - .doOnRequest( - new LongConsumer() { - - boolean firstRequest = true; + return Flux.defer( + () -> { + if (once.getAndSet(true)) { + return Flux.error( + new IllegalStateException("RequestStreamFlux allows only a single subscriber")); + } - @Override - public void accept(long n) { - if (firstRequest) { - firstRequest = false; - if (wip.getAndIncrement() != 0) { - // no need to do anything. - // stream was canceled and fist payload has already been discarded - return; - } - int missed = 1; - boolean firstHasBeenSent = false; - for (; ; ) { - if (!firstHasBeenSent) { - sendProcessor.onNext( - RequestStreamFrameFlyweight.encodeReleasingPayload( - allocator, streamId, n, payload)); - firstHasBeenSent = true; - } else { - // if first frame was sent but we cycling again, it means that wip was - // incremented at doOnCancel - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - return; + return receiver + .doOnRequest( + new LongConsumer() { + + boolean firstRequest = true; + + @Override + public void accept(long n) { + if (firstRequest) { + firstRequest = false; + if (wip.getAndIncrement() != 0) { + // no need to do anything. + // stream was canceled and fist payload has already been discarded + return; + } + int missed = 1; + boolean firstHasBeenSent = false; + for (; ; ) { + if (!firstHasBeenSent) { + sendProcessor.onNext( + RequestStreamFrameFlyweight.encodeReleasingPayload( + allocator, streamId, n, payload)); + firstHasBeenSent = true; + } else { + // if first frame was sent but we cycling again, it means that wip was + // incremented at doOnCancel + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + return; + } + + missed = wip.addAndGet(-missed); + if (missed == 0) { + return; + } + } + } else if (!receiver.isDisposed()) { + sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); + } } + }) + .doFinally( + s -> { + if (s == SignalType.CANCEL) { + if (wip.getAndIncrement() != 0) { + return; + } - missed = wip.addAndGet(-missed); - if (missed == 0) { - return; + // check if we need to release payload + // only applicable if the cancel appears earlier than actual request + if (payload.refCnt() > 0) { + payload.release(); + } else { + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + } } - } - } else if (!receiver.isDisposed()) { - sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); - } - } - }) - .doOnCancel( - () -> { - if (wip.getAndIncrement() != 0) { - return; - } - - // check if we need to release payload - // only applicable if the cancel appears earlier than actual request - if (payload.refCnt() > 0) { - payload.release(); - } else { - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - } - }) - .doFinally(s -> removeStreamReceiver(streamId)) - .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); + removeStreamReceiver(streamId); + }) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); + }); } private Flux handleChannel(Flux request) { @@ -522,12 +543,23 @@ private Mono handleMetadataPush(Payload payload) { return Mono.error(new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); } - return UnicastMonoEmpty.newInstance( + final AtomicBoolean once = new AtomicBoolean(); + + return Mono.defer( () -> { - ByteBuf metadataPushFrame = - MetadataPushFrameFlyweight.encodeReleasingPayload(allocator, payload); + if (once.getAndSet(true)) { + return Mono.error( + new IllegalStateException("MetadataPushMono allows only a single subscriber")); + } + + return Mono.empty() + .doOnSubscribe( + (__) -> { + ByteBuf metadataPushFrame = + MetadataPushFrameFlyweight.encodeReleasingPayload(allocator, payload); - sendProcessor.onNextPrioritized(metadataPushFrame); + sendProcessor.onNextPrioritized(metadataPushFrame); + }); }); } @@ -544,10 +576,6 @@ private Throwable checkAvailable() { return null; } - private boolean contains(int streamId) { - return receivers.containsKey(streamId); - } - private void handleIncomingFrames(ByteBuf frame) { try { int streamId = FrameHeaderFlyweight.streamId(frame); diff --git a/rsocket-core/src/main/java/io/rsocket/internal/BitUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/BitUtil.java deleted file mode 100644 index 79be9ccd5..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/BitUtil.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * Copyright 2014-2019 Real Logic Ltd. - * - * 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.internal; - -import static java.nio.charset.StandardCharsets.UTF_8; - -import java.util.concurrent.ThreadLocalRandom; - -/** Miscellaneous useful functions for dealing with low level bits and bytes. */ -public class BitUtil { - /** Size of a byte in bytes */ - public static final int SIZE_OF_BYTE = 1; - - /** Size of a boolean in bytes */ - public static final int SIZE_OF_BOOLEAN = 1; - - /** Size of a char in bytes */ - public static final int SIZE_OF_CHAR = 2; - - /** Size of a short in bytes */ - public static final int SIZE_OF_SHORT = 2; - - /** Size of an int in bytes */ - public static final int SIZE_OF_INT = 4; - - /** Size of a float in bytes */ - public static final int SIZE_OF_FLOAT = 4; - - /** Size of a long in bytes */ - public static final int SIZE_OF_LONG = 8; - - /** Size of a double in bytes */ - public static final int SIZE_OF_DOUBLE = 8; - - /** Length of the data blocks used by the CPU cache sub-system in bytes. */ - public static final int CACHE_LINE_LENGTH = 64; - - private static final byte[] HEX_DIGIT_TABLE = { - '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' - }; - - private static final byte[] FROM_HEX_DIGIT_TABLE; - - static { - FROM_HEX_DIGIT_TABLE = new byte[128]; - - FROM_HEX_DIGIT_TABLE['0'] = 0x00; - FROM_HEX_DIGIT_TABLE['1'] = 0x01; - FROM_HEX_DIGIT_TABLE['2'] = 0x02; - FROM_HEX_DIGIT_TABLE['3'] = 0x03; - FROM_HEX_DIGIT_TABLE['4'] = 0x04; - FROM_HEX_DIGIT_TABLE['5'] = 0x05; - FROM_HEX_DIGIT_TABLE['6'] = 0x06; - FROM_HEX_DIGIT_TABLE['7'] = 0x07; - FROM_HEX_DIGIT_TABLE['8'] = 0x08; - FROM_HEX_DIGIT_TABLE['9'] = 0x09; - FROM_HEX_DIGIT_TABLE['a'] = 0x0a; - FROM_HEX_DIGIT_TABLE['A'] = 0x0a; - FROM_HEX_DIGIT_TABLE['b'] = 0x0b; - FROM_HEX_DIGIT_TABLE['B'] = 0x0b; - FROM_HEX_DIGIT_TABLE['c'] = 0x0c; - FROM_HEX_DIGIT_TABLE['C'] = 0x0c; - FROM_HEX_DIGIT_TABLE['d'] = 0x0d; - FROM_HEX_DIGIT_TABLE['D'] = 0x0d; - FROM_HEX_DIGIT_TABLE['e'] = 0x0e; - FROM_HEX_DIGIT_TABLE['E'] = 0x0e; - FROM_HEX_DIGIT_TABLE['f'] = 0x0f; - FROM_HEX_DIGIT_TABLE['F'] = 0x0f; - } - - private static final int LAST_DIGIT_MASK = 0b1; - - /** - * Fast method of finding the next power of 2 greater than or equal to the supplied value. - * - *

    If the value is <= 0 then 1 will be returned. - * - *

    This method is not suitable for {@link Integer#MIN_VALUE} or numbers greater than 2^30. - * - * @param value from which to search for next power of 2 - * @return The next power of 2 or the value itself if it is a power of 2 - */ - public static int findNextPositivePowerOfTwo(final int value) { - return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(value - 1)); - } - - /** - * Align a value to the next multiple up of alignment. If the value equals an alignment multiple - * then it is returned unchanged. - * - *

    This method executes without branching. This code is designed to be use in the fast path and - * should not be used with negative numbers. Negative numbers will result in undefined behaviour. - * - * @param value to be aligned up. - * @param alignment to be used. - * @return the value aligned to the next boundary. - */ - public static int align(final int value, final int alignment) { - return (value + (alignment - 1)) & -alignment; - } - - /** - * Generate a byte array from the hex representation of the given byte array. - * - * @param buffer to convert from a hex representation (in Big Endian). - * @return new byte array that is decimal representation of the passed array. - */ - public static byte[] fromHexByteArray(final byte[] buffer) { - final byte[] outputBuffer = new byte[buffer.length >> 1]; - - for (int i = 0; i < buffer.length; i += 2) { - final int hi = FROM_HEX_DIGIT_TABLE[buffer[i]] << 4; - final int lo = FROM_HEX_DIGIT_TABLE[buffer[i + 1]]; // lgtm [java/index-out-of-bounds] - outputBuffer[i >> 1] = (byte) (hi | lo); - } - - return outputBuffer; - } - - /** - * Generate a byte array that is a hex representation of a given byte array. - * - * @param buffer to convert to a hex representation. - * @return new byte array that is hex representation (in Big Endian) of the passed array. - */ - public static byte[] toHexByteArray(final byte[] buffer) { - return toHexByteArray(buffer, 0, buffer.length); - } - - /** - * Generate a byte array that is a hex representation of a given byte array. - * - * @param buffer to convert to a hex representation. - * @param offset the offset into the buffer. - * @param length the number of bytes to convert. - * @return new byte array that is hex representation (in Big Endian) of the passed array. - */ - public static byte[] toHexByteArray(final byte[] buffer, final int offset, final int length) { - final byte[] outputBuffer = new byte[length << 1]; - - for (int i = 0; i < (length << 1); i += 2) { - final byte b = buffer[offset + (i >> 1)]; - - outputBuffer[i] = HEX_DIGIT_TABLE[(b >> 4) & 0x0F]; - outputBuffer[i + 1] = HEX_DIGIT_TABLE[b & 0x0F]; - } - - return outputBuffer; - } - - /** - * Generate a byte array from a string that is the hex representation of the given byte array. - * - * @param string to convert from a hex representation (in Big Endian). - * @return new byte array holding the decimal representation of the passed array. - */ - public static byte[] fromHex(final String string) { - return fromHexByteArray(string.getBytes(UTF_8)); - } - - /** - * Generate a string that is the hex representation of a given byte array. - * - * @param buffer to convert to a hex representation. - * @param offset the offset into the buffer. - * @param length the number of bytes to convert. - * @return new String holding the hex representation (in Big Endian) of the passed array. - */ - public static String toHex(final byte[] buffer, final int offset, final int length) { - return new String(toHexByteArray(buffer, offset, length), UTF_8); - } - - /** - * Generate a string that is the hex representation of a given byte array. - * - * @param buffer to convert to a hex representation. - * @return new String holding the hex representation (in Big Endian) of the passed array. - */ - public static String toHex(final byte[] buffer) { - return new String(toHexByteArray(buffer), UTF_8); - } - - /** - * Is a number even. - * - * @param value to check. - * @return true if the number is even otherwise false. - */ - public static boolean isEven(final int value) { - return (value & LAST_DIGIT_MASK) == 0; - } - - /** - * Is a value a positive power of 2. - * - * @param value to be checked. - * @return true if the number is a positive power of 2, otherwise false. - */ - public static boolean isPowerOfTwo(final int value) { - return value > 0 && ((value & (~value + 1)) == value); - } - - /** - * Cycles indices of an array one at a time in a forward fashion - * - * @param current value to be incremented. - * @param max value for the cycle. - * @return the next value, or zero if max is reached. - */ - public static int next(final int current, final int max) { - int next = current + 1; - if (next == max) { - next = 0; - } - - return next; - } - - /** - * Cycles indices of an array one at a time in a backwards fashion - * - * @param current value to be decremented. - * @param max value of the cycle. - * @return the next value, or max - 1 if current is zero. - */ - public static int previous(final int current, final int max) { - if (0 == current) { - return max - 1; - } - - return current - 1; - } - - /** - * Calculate the shift value to scale a number based on how refs are compressed or not. - * - * @param scale of the number reported by Unsafe. - * @return how many times the number needs to be shifted to the left. - */ - public static int calculateShiftForScale(final int scale) { - if (4 == scale) { - return 2; - } else if (8 == scale) { - return 3; - } - - throw new IllegalArgumentException("unknown pointer size for scale=" + scale); - } - - /** - * Generate a randomised integer over [{@link Integer#MIN_VALUE}, {@link Integer#MAX_VALUE}]. - * - * @return randomised integer suitable as an Id. - */ - public static int generateRandomisedId() { - return ThreadLocalRandom.current().nextInt(); - } - - /** - * Is an address aligned on a boundary. - * - * @param address to be tested. - * @param alignment boundary the address is tested against. - * @return true if the address is on the aligned boundary otherwise false. - * @throws IllegalArgumentException if the alignment is not a power of 2. - */ - public static boolean isAligned(final long address, final int alignment) { - if (!BitUtil.isPowerOfTwo(alignment)) { - throw new IllegalArgumentException("alignment must be a power of 2: alignment=" + alignment); - } - - return (address & (alignment - 1)) == 0; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/CollectionUtil.java b/rsocket-core/src/main/java/io/rsocket/internal/CollectionUtil.java deleted file mode 100644 index 8d4526c36..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/CollectionUtil.java +++ /dev/null @@ -1,121 +0,0 @@ -/* - * Copyright 2014-2019 Real Logic Ltd. - * - * 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.internal; - -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.ToIntFunction; - -/** Utility functions for collection objects. */ -public class CollectionUtil { - /** - * A getOrDefault that doesn't create garbage if its suppler is non-capturing. - * - * @param map to perform the lookup on. - * @param key on which the lookup is done. - * @param supplier of the default value if one is not found. - * @param type of the key - * @param type of the value - * @return the value if found or a new default which as been added to the map. - */ - public static V getOrDefault( - final Map map, final K key, final Function supplier) { - V value = map.get(key); - if (value == null) { - value = supplier.apply(key); - map.put(key, value); - } - - return value; - } - - /** - * Garbage free sum function. - * - *

    Note: the list must implement {@link java.util.RandomAccess} to be efficient. - * - * @param values the list of input values - * @param function function that map each value to an int - * @param the value to add up - * @return the sum of all the int values returned for each member of the list. - */ - public static int sum(final List values, final ToIntFunction function) { - int total = 0; - - final int size = values.size(); - for (int i = 0; i < size; i++) { - final V value = values.get(i); - total += function.applyAsInt(value); - } - - return total; - } - - /** - * Validate that a load factor is in the range of 0.1 to 0.9. - * - *

    Load factors in the range 0.5 - 0.7 are recommended for open-addressing with linear probing. - * - * @param loadFactor to be validated. - */ - public static void validateLoadFactor(final float loadFactor) { - if (loadFactor < 0.1f || loadFactor > 0.9f) { - throw new IllegalArgumentException( - "load factor must be in the range of 0.1 to 0.9: " + loadFactor); - } - } - - /** - * Validate that a number is a power of two. - * - * @param value to be validated. - */ - public static void validatePositivePowerOfTwo(final int value) { - if (value > 0 && 1 == (value & (value - 1))) { - throw new IllegalStateException("value must be a positive power of two"); - } - } - - /** - * Remove element from a list if it matches a predicate. - * - *

    Note: the list must implement {@link java.util.RandomAccess} to be efficient. - * - * @param values to be iterated over. - * @param predicate to test the value against - * @param type of the value. - * @return the number of items remove. - */ - public static int removeIf(final List values, final Predicate predicate) { - int size = values.size(); - int total = 0; - - for (int i = 0; i < size; ) { - final T value = values.get(i); - if (predicate.test(value)) { - values.remove(i); - total++; - size--; - } else { - i++; - } - } - - return total; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/Hashing.java b/rsocket-core/src/main/java/io/rsocket/internal/Hashing.java deleted file mode 100644 index 613dce209..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/Hashing.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2014-2019 Real Logic Ltd. - * - * 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.internal; - -/** Hashing functions for applying to integers. */ -public class Hashing { - /** Default load factor to be used in open addressing hashed data structures. */ - public static final float DEFAULT_LOAD_FACTOR = 0.55f; - - /** - * Generate a hash for an int value. This is a no op. - * - * @param value to be hashed. - * @return the hashed value. - */ - public static int hash(final int value) { - return value * 31; - } - - /** - * Generate a hash for an long value. - * - * @param value to be hashed. - * @return the hashed value. - */ - public static int hash(final long value) { - long hash = value * 31; - hash = (int) hash ^ (int) (hash >>> 32); - - return (int) hash; - } - - /** - * Generate a hash for a int value. - * - * @param value to be hashed. - * @param mask mask to be applied that must be a power of 2 - 1. - * @return the hash of the value. - */ - public static int hash(final int value, final int mask) { - final int hash = value * 31; - - return hash & mask; - } - - /** - * Generate a hash for a K value. - * - * @param is the type of value - * @param value to be hashed. - * @param mask mask to be applied that must be a power of 2 - 1. - * @return the hash of the value. - */ - public static int hash(final K value, final int mask) { - final int hash = value.hashCode(); - - return hash & mask; - } - - /** - * Generate a hash for a long value. - * - * @param value to be hashed. - * @param mask mask to be applied that must be a power of 2 - 1. - * @return the hash of the value. - */ - public static int hash(final long value, final int mask) { - long hash = value * 31; - hash = (int) hash ^ (int) (hash >>> 32); - - return (int) hash & mask; - } - - /** - * Generate an even hash for a int value. - * - * @param value to be hashed. - * @param mask mask to be applied that must be a power of 2 - 1. - * @return the hash of the value which is always even. - */ - public static int evenHash(final int value, final int mask) { - final int hash = (value << 1) - (value << 8); - - return hash & mask; - } - - /** - * Generate an even hash for a long value. - * - * @param value to be hashed. - * @param mask mask to be applied that must be a power of 2 - 1. - * @return the hash of the value which is always even. - */ - public static int evenHash(final long value, final int mask) { - int hash = (int) value ^ (int) (value >>> 32); - hash = (hash << 1) - (hash << 8); - - return hash & mask; - } - - /** - * Combined two 32 bit keys into a 64-bit compound. - * - * @param keyPartA to make the upper bits - * @param keyPartB to make the lower bits. - * @return the compound key - */ - public static long compoundKey(final int keyPartA, final int keyPartB) { - return ((long) keyPartA << 32) | (keyPartB & 0xFFFF_FFFFL); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java b/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java deleted file mode 100755 index 8adb7542a..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/LimitableRequestPublisher.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright 2015-2018 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.internal; - -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import javax.annotation.Nullable; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.CoreSubscriber; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Operators; - -/** */ -public class LimitableRequestPublisher extends Flux implements Subscription { - - private static final int NOT_CANCELED_STATE = 0; - private static final int CANCELED_STATE = 1; - - private final Publisher source; - - private volatile int canceled; - private static final AtomicIntegerFieldUpdater CANCELED = - AtomicIntegerFieldUpdater.newUpdater(LimitableRequestPublisher.class, "canceled"); - - private final long prefetch; - - private long internalRequested; - - private long externalRequested; - - private boolean subscribed; - - private @Nullable Subscription internalSubscription; - - private LimitableRequestPublisher(Publisher source, long prefetch) { - this.source = source; - this.prefetch = prefetch; - } - - public static LimitableRequestPublisher wrap(Publisher source, long prefetch) { - return new LimitableRequestPublisher<>(source, prefetch); - } - - public static LimitableRequestPublisher wrap(Publisher source) { - return wrap(source, Long.MAX_VALUE); - } - - @Override - public void subscribe(CoreSubscriber destination) { - synchronized (this) { - if (subscribed) { - throw new IllegalStateException("only one subscriber at a time"); - } - - subscribed = true; - } - final InnerOperator s = new InnerOperator(destination); - - destination.onSubscribe(s); - source.subscribe(s); - increaseInternalLimit(prefetch); - } - - public void increaseInternalLimit(long n) { - synchronized (this) { - long requested = internalRequested; - if (requested == Long.MAX_VALUE) { - return; - } - internalRequested = Operators.addCap(n, requested); - } - - requestN(); - } - - @Override - public void request(long n) { - synchronized (this) { - long requested = externalRequested; - if (requested == Long.MAX_VALUE) { - return; - } - externalRequested = Operators.addCap(n, requested); - } - - requestN(); - } - - private void requestN() { - long r; - final Subscription s; - - synchronized (this) { - s = internalSubscription; - if (s == null) { - return; - } - - long er = externalRequested; - long ir = internalRequested; - - if (er != Long.MAX_VALUE || ir != Long.MAX_VALUE) { - r = Math.min(ir, er); - if (er != Long.MAX_VALUE) { - externalRequested -= r; - } - if (ir != Long.MAX_VALUE) { - internalRequested -= r; - } - } else { - r = Long.MAX_VALUE; - } - } - - if (r > 0) { - s.request(r); - } - } - - public void cancel() { - if (!isCanceled() && CANCELED.compareAndSet(this, NOT_CANCELED_STATE, CANCELED_STATE)) { - Subscription s; - - synchronized (this) { - s = internalSubscription; - internalSubscription = null; - subscribed = false; - } - - if (s != null) { - s.cancel(); - } - } - } - - private boolean isCanceled() { - return canceled == 1; - } - - private class InnerOperator implements CoreSubscriber, Subscription { - final Subscriber destination; - - private InnerOperator(Subscriber destination) { - this.destination = destination; - } - - @Override - public void onSubscribe(Subscription s) { - synchronized (LimitableRequestPublisher.this) { - LimitableRequestPublisher.this.internalSubscription = s; - - if (isCanceled()) { - s.cancel(); - subscribed = false; - LimitableRequestPublisher.this.internalSubscription = null; - } - } - - requestN(); - } - - @Override - public void onNext(T t) { - try { - destination.onNext(t); - } catch (Throwable e) { - onError(e); - } - } - - @Override - public void onError(Throwable t) { - destination.onError(t); - } - - @Override - public void onComplete() { - destination.onComplete(); - } - - @Override - public void request(long n) {} - - @Override - public void cancel() { - LimitableRequestPublisher.this.cancel(); - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/SwitchTransformFlux.java b/rsocket-core/src/main/java/io/rsocket/internal/SwitchTransformFlux.java deleted file mode 100644 index 0d2e5988e..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/SwitchTransformFlux.java +++ /dev/null @@ -1,581 +0,0 @@ -/* - * Copyright 2015-2018 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.internal; - -import java.util.Objects; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -import java.util.function.BiFunction; -import org.reactivestreams.Publisher; -import org.reactivestreams.Subscription; -import reactor.core.CoreSubscriber; -import reactor.core.Fuseable; -import reactor.core.Scannable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Operators; -import reactor.util.annotation.Nullable; -import reactor.util.context.Context; - -/** @deprecated in favour of {@link Flux#switchOnFirst(BiFunction)} */ -@Deprecated -public final class SwitchTransformFlux extends Flux { - - final Publisher source; - final BiFunction, Publisher> transformer; - - public SwitchTransformFlux( - Publisher source, BiFunction, Publisher> transformer) { - this.source = Objects.requireNonNull(source, "source"); - this.transformer = Objects.requireNonNull(transformer, "transformer"); - } - - @Override - public int getPrefetch() { - return 1; - } - - @Override - @SuppressWarnings("unchecked") - public void subscribe(CoreSubscriber actual) { - if (actual instanceof Fuseable.ConditionalSubscriber) { - source.subscribe( - new SwitchTransformConditionalOperator<>( - (Fuseable.ConditionalSubscriber) actual, transformer)); - return; - } - source.subscribe(new SwitchTransformOperator<>(actual, transformer)); - } - - static final class SwitchTransformOperator extends Flux - implements CoreSubscriber, Subscription, Scannable { - - final CoreSubscriber outer; - final BiFunction, Publisher> transformer; - - Subscription s; - Throwable throwable; - - volatile boolean done; - volatile T first; - - volatile CoreSubscriber inner; - - @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater INNER = - AtomicReferenceFieldUpdater.newUpdater( - SwitchTransformOperator.class, CoreSubscriber.class, "inner"); - - volatile int wip; - - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater WIP = - AtomicIntegerFieldUpdater.newUpdater(SwitchTransformOperator.class, "wip"); - - volatile int once; - - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater ONCE = - AtomicIntegerFieldUpdater.newUpdater(SwitchTransformOperator.class, "once"); - - SwitchTransformOperator( - CoreSubscriber outer, - BiFunction, Publisher> transformer) { - this.outer = outer; - this.transformer = transformer; - } - - @Override - @Nullable - public Object scanUnsafe(Attr key) { - if (key == Attr.CANCELLED) return s == Operators.cancelledSubscription(); - if (key == Attr.PREFETCH) return 1; - - return null; - } - - @Override - public Context currentContext() { - CoreSubscriber actual = inner; - - if (actual != null) { - return actual.currentContext(); - } - - return outer.currentContext(); - } - - @Override - public void cancel() { - if (s != Operators.cancelledSubscription()) { - Subscription s = this.s; - this.s = Operators.cancelledSubscription(); - - if (WIP.getAndIncrement(this) == 0) { - INNER.lazySet(this, null); - - T f = first; - if (f != null) { - first = null; - Operators.onDiscard(f, currentContext()); - } - } - - s.cancel(); - } - } - - @Override - public void subscribe(CoreSubscriber actual) { - if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { - INNER.lazySet(this, actual); - actual.onSubscribe(this); - } else { - Operators.error( - actual, new IllegalStateException("SwitchTransform allows only one Subscriber")); - } - } - - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - s.request(1); - } - } - - @Override - public void onNext(T t) { - if (done) { - Operators.onNextDropped(t, currentContext()); - return; - } - - CoreSubscriber i = inner; - - if (i == null) { - try { - first = t; - Publisher result = - Objects.requireNonNull( - transformer.apply(t, this), "The transformer returned a null value"); - result.subscribe(outer); - return; - } catch (Throwable e) { - onError(Operators.onOperatorError(s, e, t, currentContext())); - return; - } - } - - i.onNext(t); - } - - @Override - public void onError(Throwable t) { - if (done) { - Operators.onErrorDropped(t, currentContext()); - return; - } - - throwable = t; - done = true; - CoreSubscriber i = inner; - - if (i != null) { - if (first == null) { - drainRegular(); - } - } else { - Operators.error(outer, t); - } - } - - @Override - public void onComplete() { - if (done) { - return; - } - - done = true; - CoreSubscriber i = inner; - - if (i != null) { - if (first == null) { - drainRegular(); - } - } else { - Operators.complete(outer); - } - } - - @Override - public void request(long n) { - if (Operators.validate(n)) { - if (first != null && drainRegular() && n != Long.MAX_VALUE) { - if (--n > 0) { - s.request(n); - } - } else { - s.request(n); - } - } - } - - boolean drainRegular() { - if (WIP.getAndIncrement(this) != 0) { - return false; - } - - T f = first; - int m = 1; - boolean sent = false; - Subscription s = this.s; - CoreSubscriber a = inner; - - for (; ; ) { - if (f != null) { - first = null; - - if (s == Operators.cancelledSubscription()) { - Operators.onDiscard(f, a.currentContext()); - return true; - } - - a.onNext(f); - f = null; - sent = true; - } - - if (s == Operators.cancelledSubscription()) { - return sent; - } - - if (done) { - Throwable t = throwable; - if (t != null) { - a.onError(t); - } else { - a.onComplete(); - } - return sent; - } - - m = WIP.addAndGet(this, -m); - - if (m == 0) { - return sent; - } - } - } - } - - static final class SwitchTransformConditionalOperator extends Flux - implements Fuseable.ConditionalSubscriber, Subscription, Scannable { - - final Fuseable.ConditionalSubscriber outer; - final BiFunction, Publisher> transformer; - - Subscription s; - Throwable throwable; - - volatile boolean done; - volatile T first; - - volatile Fuseable.ConditionalSubscriber inner; - - @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater< - SwitchTransformConditionalOperator, Fuseable.ConditionalSubscriber> - INNER = - AtomicReferenceFieldUpdater.newUpdater( - SwitchTransformConditionalOperator.class, - Fuseable.ConditionalSubscriber.class, - "inner"); - - volatile int wip; - - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater WIP = - AtomicIntegerFieldUpdater.newUpdater(SwitchTransformConditionalOperator.class, "wip"); - - volatile int once; - - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater ONCE = - AtomicIntegerFieldUpdater.newUpdater(SwitchTransformConditionalOperator.class, "once"); - - SwitchTransformConditionalOperator( - Fuseable.ConditionalSubscriber outer, - BiFunction, Publisher> transformer) { - this.outer = outer; - this.transformer = transformer; - } - - @Override - @Nullable - public Object scanUnsafe(Attr key) { - if (key == Attr.CANCELLED) return s == Operators.cancelledSubscription(); - if (key == Attr.PREFETCH) return 1; - - return null; - } - - @Override - public Context currentContext() { - CoreSubscriber actual = inner; - - if (actual != null) { - return actual.currentContext(); - } - - return outer.currentContext(); - } - - @Override - public void cancel() { - if (s != Operators.cancelledSubscription()) { - Subscription s = this.s; - this.s = Operators.cancelledSubscription(); - - if (WIP.getAndIncrement(this) == 0) { - INNER.lazySet(this, null); - - T f = first; - if (f != null) { - first = null; - Operators.onDiscard(f, currentContext()); - } - } - - s.cancel(); - } - } - - @Override - @SuppressWarnings("unchecked") - public void subscribe(CoreSubscriber actual) { - if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { - if (actual instanceof Fuseable.ConditionalSubscriber) { - INNER.lazySet(this, (Fuseable.ConditionalSubscriber) actual); - } else { - INNER.lazySet(this, new ConditionalSubscriberAdapter<>(actual)); - } - actual.onSubscribe(this); - } else { - Operators.error( - actual, new IllegalStateException("SwitchTransform allows only one Subscriber")); - } - } - - @Override - public void onSubscribe(Subscription s) { - if (Operators.validate(this.s, s)) { - this.s = s; - s.request(1); - } - } - - @Override - public void onNext(T t) { - if (done) { - Operators.onNextDropped(t, currentContext()); - return; - } - - CoreSubscriber i = inner; - - if (i == null) { - try { - first = t; - Publisher result = - Objects.requireNonNull( - transformer.apply(t, this), "The transformer returned a null value"); - result.subscribe(outer); - return; - } catch (Throwable e) { - onError(Operators.onOperatorError(s, e, t, currentContext())); - return; - } - } - - i.onNext(t); - } - - @Override - public boolean tryOnNext(T t) { - if (done) { - Operators.onNextDropped(t, currentContext()); - return false; - } - - Fuseable.ConditionalSubscriber i = inner; - - if (i == null) { - try { - first = t; - Publisher result = - Objects.requireNonNull( - transformer.apply(t, this), "The transformer returned a null value"); - result.subscribe(outer); - return true; - } catch (Throwable e) { - onError(Operators.onOperatorError(s, e, t, currentContext())); - return false; - } - } - - return i.tryOnNext(t); - } - - @Override - public void onError(Throwable t) { - if (done) { - Operators.onErrorDropped(t, currentContext()); - return; - } - - throwable = t; - done = true; - CoreSubscriber i = inner; - - if (i != null) { - if (first == null) { - drainRegular(); - } - } else { - Operators.error(outer, t); - } - } - - @Override - public void onComplete() { - if (done) { - return; - } - - done = true; - CoreSubscriber i = inner; - - if (i != null) { - if (first == null) { - drainRegular(); - } - } else { - Operators.complete(outer); - } - } - - @Override - public void request(long n) { - if (Operators.validate(n)) { - if (first != null && drainRegular() && n != Long.MAX_VALUE) { - if (--n > 0) { - s.request(n); - } - } else { - s.request(n); - } - } - } - - boolean drainRegular() { - if (WIP.getAndIncrement(this) != 0) { - return false; - } - - T f = first; - int m = 1; - boolean sent = false; - Subscription s = this.s; - CoreSubscriber a = inner; - - for (; ; ) { - if (f != null) { - first = null; - - if (s == Operators.cancelledSubscription()) { - Operators.onDiscard(f, a.currentContext()); - return true; - } - - a.onNext(f); - f = null; - sent = true; - } - - if (s == Operators.cancelledSubscription()) { - return sent; - } - - if (done) { - Throwable t = throwable; - if (t != null) { - a.onError(t); - } else { - a.onComplete(); - } - return sent; - } - - m = WIP.addAndGet(this, -m); - - if (m == 0) { - return sent; - } - } - } - } - - static final class ConditionalSubscriberAdapter implements Fuseable.ConditionalSubscriber { - - final CoreSubscriber delegate; - - ConditionalSubscriberAdapter(CoreSubscriber delegate) { - this.delegate = delegate; - } - - @Override - public Context currentContext() { - return delegate.currentContext(); - } - - @Override - public void onSubscribe(Subscription s) { - delegate.onSubscribe(s); - } - - @Override - public void onNext(T t) { - delegate.onNext(t); - } - - @Override - public void onError(Throwable t) { - delegate.onError(t); - } - - @Override - public void onComplete() { - delegate.onComplete(); - } - - @Override - public boolean tryOnNext(T t) { - delegate.onNext(t); - return true; - } - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoEmpty.java b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoEmpty.java deleted file mode 100644 index 64a7d4422..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoEmpty.java +++ /dev/null @@ -1,84 +0,0 @@ -package io.rsocket.internal; - -import java.time.Duration; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import reactor.core.CoreSubscriber; -import reactor.core.Scannable; -import reactor.core.publisher.Mono; -import reactor.core.publisher.Operators; -import reactor.util.annotation.Nullable; - -/** - * Represents an empty publisher which only calls onSubscribe and onComplete and allows only a - * single subscriber. - * - * @see Reactive-Streams-Commons - */ -public final class UnicastMonoEmpty extends Mono implements Scannable { - - final Runnable onSubscribe; - - volatile int once; - - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater ONCE = - AtomicIntegerFieldUpdater.newUpdater(UnicastMonoEmpty.class, "once"); - - UnicastMonoEmpty(Runnable onSubscribe) { - this.onSubscribe = onSubscribe; - } - - @Override - public void subscribe(CoreSubscriber actual) { - if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { - onSubscribe.run(); - Operators.complete(actual); - } else { - Operators.error( - actual, new IllegalStateException("UnicastMonoEmpty allows only a single Subscriber")); - } - } - - /** - * Returns a properly parametrized instance of this empty Publisher. - * - * @param the output type - * @return a properly parametrized instance of this empty Publisher - */ - @SuppressWarnings("unchecked") - public static Mono newInstance(Runnable onSubscribe) { - return (Mono) new UnicastMonoEmpty(onSubscribe); - } - - @Override - @Nullable - public Object block(Duration m) { - if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { - onSubscribe.run(); - return null; - } else { - throw new IllegalStateException("UnicastMonoEmpty allows only a single Subscriber"); - } - } - - @Override - @Nullable - public Object block() { - if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { - onSubscribe.run(); - return null; - } else { - throw new IllegalStateException("UnicastMonoEmpty allows only a single Subscriber"); - } - } - - @Override - public Object scanUnsafe(Attr key) { - return null; // no particular key to be represented, still useful in hooks - } - - @Override - public String stepName() { - return "source(UnicastMonoEmpty)"; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java b/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java deleted file mode 100644 index c5b06e086..000000000 --- a/rsocket-core/src/main/java/io/rsocket/internal/UnicastMonoProcessor.java +++ /dev/null @@ -1,509 +0,0 @@ -/* - * Copyright 2015-2019 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.internal; - -import io.rsocket.util.MonoLifecycleHandler; -import java.util.Objects; -import java.util.concurrent.CancellationException; -import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; -import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; -import org.reactivestreams.Processor; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.CoreSubscriber; -import reactor.core.Disposable; -import reactor.core.Exceptions; -import reactor.core.Scannable; -import reactor.core.publisher.Mono; -import reactor.core.publisher.Operators; -import reactor.core.publisher.SignalType; -import reactor.util.annotation.NonNull; -import reactor.util.annotation.Nullable; -import reactor.util.context.Context; - -public class UnicastMonoProcessor extends Mono - implements Processor, CoreSubscriber, Disposable, Subscription, Scannable { - - static final MonoLifecycleHandler DEFAULT_LIFECYCLE = new MonoLifecycleHandler() {}; - - /** - * Create a {@link UnicastMonoProcessor} that will eagerly request 1 on {@link - * #onSubscribe(Subscription)}, cache and emit the eventual result for a single subscriber. - * - * @param type of the expected value - * @return A {@link UnicastMonoProcessor}. - */ - @SuppressWarnings("unchecked") - public static UnicastMonoProcessor create() { - return new UnicastMonoProcessor(DEFAULT_LIFECYCLE); - } - - /** - * Create a {@link UnicastMonoProcessor} that will eagerly request 1 on {@link - * #onSubscribe(Subscription)}, cache and emit the eventual result for a single subscriber. - * - * @param lifecycleHandler lifecycle handler - * @param type of the expected value - * @return A {@link UnicastMonoProcessor}. - */ - public static UnicastMonoProcessor create(MonoLifecycleHandler lifecycleHandler) { - return new UnicastMonoProcessor<>(lifecycleHandler); - } - - /** Indicates this Subscription has no value and not requested yet. */ - static final int NO_SUBSCRIBER_NO_RESULT = 0; - /** Indicates this Subscription has no value and not requested yet. */ - static final int NO_SUBSCRIBER_HAS_RESULT = 1; - /** Indicates this Subscription has no value and not requested yet. */ - static final int NO_REQUEST_NO_RESULT = 4; - /** Indicates this Subscription has a value but not requested yet. */ - static final int NO_REQUEST_HAS_RESULT = 5; - /** Indicates this Subscription has been requested but there is no value yet. */ - static final int HAS_REQUEST_NO_RESULT = 6; - /** Indicates this Subscription has both request and value. */ - static final int HAS_REQUEST_HAS_RESULT = 7; - /** Indicates the Subscription has been cancelled. */ - static final int CANCELLED = 8; - - volatile int state; - - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater STATE = - AtomicIntegerFieldUpdater.newUpdater(UnicastMonoProcessor.class, "state"); - - volatile int once; - - @SuppressWarnings("rawtypes") - static final AtomicIntegerFieldUpdater ONCE = - AtomicIntegerFieldUpdater.newUpdater(UnicastMonoProcessor.class, "once"); - - volatile Subscription subscription; - - @SuppressWarnings("rawtypes") - static final AtomicReferenceFieldUpdater UPSTREAM = - AtomicReferenceFieldUpdater.newUpdater( - UnicastMonoProcessor.class, Subscription.class, "subscription"); - - CoreSubscriber actual; - boolean hasDownstream = false; - - Throwable error; - O value; - - final MonoLifecycleHandler lifecycleHandler; - - UnicastMonoProcessor(MonoLifecycleHandler lifecycleHandler) { - this.lifecycleHandler = lifecycleHandler; - } - - @Override - @NonNull - public Context currentContext() { - final CoreSubscriber a = this.actual; - return a != null ? a.currentContext() : Context.empty(); - } - - @Override - public final void onSubscribe(Subscription subscription) { - if (Operators.setOnce(UPSTREAM, this, subscription)) { - subscription.request(Long.MAX_VALUE); - } - } - - @Override - public final void onComplete() { - onNext(null); - } - - @Override - public final void onError(Throwable cause) { - Objects.requireNonNull(cause, "onError cannot be null"); - - if (UPSTREAM.getAndSet(this, Operators.cancelledSubscription()) - == Operators.cancelledSubscription()) { - Operators.onErrorDropped(cause, currentContext()); - return; - } - - complete(cause); - } - - @Override - public final void onNext(@Nullable O value) { - final Subscription s; - if ((s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription())) - == Operators.cancelledSubscription()) { - if (value != null) { - Operators.onNextDropped(value, currentContext()); - } - return; - } - - if (value == null) { - complete(); - } else { - if (s != null) { - s.cancel(); - } - - complete(value); - } - } - - /** - * Tries to emit the value and complete the underlying subscriber or stores the value away until - * there is a request for it. - * - *

    Make sure this method is called at most once - * - * @param v the value to emit - */ - private void complete(O v) { - for (; ; ) { - int state = this.state; - - // if state is >= HAS_CANCELLED or bit zero is set (*_HAS_VALUE) case, return - if ((state & ~HAS_REQUEST_NO_RESULT) != 0) { - this.value = null; - Operators.onDiscard(v, currentContext()); - return; - } - - if (state == HAS_REQUEST_NO_RESULT) { - if (STATE.compareAndSet(this, HAS_REQUEST_NO_RESULT, HAS_REQUEST_HAS_RESULT)) { - final Subscriber a = actual; - hasDownstream = false; - value = null; - lifecycleHandler.doOnTerminal(SignalType.ON_COMPLETE, v, null); - a.onNext(v); - a.onComplete(); - return; - } - } - setValue(v); - if (state == NO_REQUEST_NO_RESULT - && STATE.compareAndSet(this, NO_REQUEST_NO_RESULT, NO_REQUEST_HAS_RESULT)) { - return; - } - if (state == NO_SUBSCRIBER_NO_RESULT - && STATE.compareAndSet(this, NO_SUBSCRIBER_NO_RESULT, NO_SUBSCRIBER_HAS_RESULT)) { - return; - } - } - } - - /** - * Tries to emit completion the underlying subscriber - * - *

    Make sure this method is called at most once - */ - private void complete() { - for (; ; ) { - int state = this.state; - - // if state is >= HAS_CANCELLED or bit zero is set (*_HAS_VALUE) case, return - if ((state & ~HAS_REQUEST_NO_RESULT) != 0) { - return; - } - - if (state == HAS_REQUEST_NO_RESULT || state == NO_REQUEST_NO_RESULT) { - if (STATE.compareAndSet(this, state, HAS_REQUEST_HAS_RESULT)) { - final Subscriber a = actual; - hasDownstream = false; - lifecycleHandler.doOnTerminal(SignalType.ON_COMPLETE, null, null); - a.onComplete(); - return; - } - } - if (state == NO_SUBSCRIBER_NO_RESULT - && STATE.compareAndSet(this, NO_SUBSCRIBER_NO_RESULT, NO_SUBSCRIBER_HAS_RESULT)) { - return; - } - } - } - - /** - * Tries to emit error the underlying subscriber or stores the value away until there is a request - * for it. - * - *

    Make sure this method is called at most once - * - * @param e the error to emit - */ - private void complete(Throwable e) { - for (; ; ) { - int state = this.state; - - // if state is >= HAS_CANCELLED or bit zero is set (*_HAS_VALUE) case, return - if ((state & ~HAS_REQUEST_NO_RESULT) != 0) { - return; - } - - setError(e); - if (state == HAS_REQUEST_NO_RESULT || state == NO_REQUEST_NO_RESULT) { - if (STATE.compareAndSet(this, state, HAS_REQUEST_HAS_RESULT)) { - final Subscriber a = actual; - hasDownstream = false; - lifecycleHandler.doOnTerminal(SignalType.ON_ERROR, null, e); - a.onError(e); - return; - } - } - if (state == NO_SUBSCRIBER_NO_RESULT - && STATE.compareAndSet(this, NO_SUBSCRIBER_NO_RESULT, NO_SUBSCRIBER_HAS_RESULT)) { - return; - } - } - } - - @Override - public void subscribe(CoreSubscriber actual) { - Objects.requireNonNull(actual, "subscribe"); - - if (once == 0 && ONCE.compareAndSet(this, 0, 1)) { - final MonoLifecycleHandler lh = this.lifecycleHandler; - - lh.doOnSubscribe(); - - this.hasDownstream = true; - this.actual = actual; - - int state = this.state; - - // possible states within the racing between [onNext / onComplete / onError / dispose] and - // setting subscriber - // are NO_SUBSCRIBER_[NO_RESULT or HAS_RESULT] - if (state == NO_SUBSCRIBER_NO_RESULT) { - if (STATE.compareAndSet(this, NO_SUBSCRIBER_NO_RESULT, NO_REQUEST_NO_RESULT)) { - state = NO_REQUEST_NO_RESULT; - } else { - // the possible false position is racing with [onNext / onError / onComplete / dispose] - // which are going to put the state in the NO_REQUEST_HAS_RESULT - STATE.set(this, NO_REQUEST_HAS_RESULT); - state = NO_REQUEST_HAS_RESULT; - } - } else { - STATE.set(this, NO_REQUEST_HAS_RESULT); - state = NO_REQUEST_HAS_RESULT; - } - - // check if state is with a result then there is a chance of immediate termination if there is - // no value - // e.g. [onError / onComplete / dispose] only - if (state == NO_REQUEST_HAS_RESULT && this.value == null) { - this.hasDownstream = false; - Throwable e = this.error; - // barrier to flush changes - STATE.set(this, HAS_REQUEST_HAS_RESULT); - if (e == null) { - lh.doOnTerminal(SignalType.ON_COMPLETE, null, null); - Operators.complete(actual); - } else { - lh.doOnTerminal(SignalType.ON_ERROR, null, e); - Operators.error(actual, e); - } - return; - } - - // call onSubscribe if has value in the result or no result delivered so far - actual.onSubscribe(this); - } else { - Operators.error( - actual, - new IllegalStateException("UnicastMonoProcessor allows only a single Subscriber")); - } - } - - @Override - public final void request(long n) { - if (Operators.validate(n)) { - for (; ; ) { - int s = state; - // if the any bits 1-31 are set, we are either in fusion mode (FUSED_*) - // or request has been called (HAS_REQUEST_*) - if ((s & ~NO_REQUEST_HAS_RESULT) != 0) { - return; - } - if (s == NO_REQUEST_HAS_RESULT) { - if (STATE.compareAndSet(this, NO_REQUEST_HAS_RESULT, HAS_REQUEST_HAS_RESULT)) { - final Subscriber a = actual; - final O v = value; - hasDownstream = false; - value = null; - lifecycleHandler.doOnTerminal(SignalType.ON_COMPLETE, v, null); - a.onNext(v); - a.onComplete(); - return; - } - } - if (STATE.compareAndSet(this, NO_REQUEST_NO_RESULT, HAS_REQUEST_NO_RESULT)) { - return; - } - } - } - } - - @Override - public final void cancel() { - if (STATE.getAndSet(this, CANCELLED) <= HAS_REQUEST_NO_RESULT) { - Operators.onDiscard(value, currentContext()); - value = null; - hasDownstream = false; - lifecycleHandler.doOnTerminal(SignalType.CANCEL, null, null); - final Subscription s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription()); - if (s != null && s != Operators.cancelledSubscription()) { - s.cancel(); - } - } - } - - @Override - public void dispose() { - final Subscription s = UPSTREAM.getAndSet(this, Operators.cancelledSubscription()); - if (s == Operators.cancelledSubscription()) { - return; - } - - if (s != null) { - s.cancel(); - } - - complete(new CancellationException("Disposed")); - } - - /** - * Returns the value that completed this {@link UnicastMonoProcessor}. Returns {@code null} if the - * {@link UnicastMonoProcessor} has not been completed. If the {@link UnicastMonoProcessor} is - * completed with an error a RuntimeException that wraps the error is thrown. - * - * @return the value that completed the {@link UnicastMonoProcessor}, or {@code null} if it has - * not been completed - * @throws RuntimeException if the {@link UnicastMonoProcessor} was completed with an error - */ - @Nullable - public O peek() { - if (isCancelled()) { - return null; - } - - if (value != null) { - return value; - } - - if (error != null) { - RuntimeException re = Exceptions.propagate(error); - re = Exceptions.addSuppressed(re, new Exception("Mono#peek terminated with an error")); - throw re; - } - - return null; - } - - /** - * Set the value internally, without impacting request tracking state. - * - * @param value the new value. - * @see #complete(Object) - */ - private void setValue(O value) { - this.value = value; - } - - /** - * Set the error internally, without impacting request tracking state. - * - * @param throwable the error. - * @see #complete(Object) - */ - private void setError(Throwable throwable) { - this.error = throwable; - } - - /** - * Return the produced {@link Throwable} error if any or null - * - * @return the produced {@link Throwable} error if any or null - */ - @Nullable - public final Throwable getError() { - return isDisposed() ? error : null; - } - - /** - * Indicates whether this {@code UnicastMonoProcessor} has been completed with an error. - * - * @return {@code true} if this {@code UnicastMonoProcessor} was completed with an error, {@code - * false} otherwise. - */ - public final boolean isError() { - return getError() != null; - } - - /** - * Indicates whether this {@code UnicastMonoProcessor} has been interrupted via cancellation. - * - * @return {@code true} if this {@code UnicastMonoProcessor} is cancelled, {@code false} - * otherwise. - */ - public boolean isCancelled() { - return state == CANCELLED; - } - - public final boolean isTerminated() { - int state = this.state; - return (state < CANCELLED && state % 2 == 1); - } - - @Override - public boolean isDisposed() { - int state = this.state; - return state == CANCELLED || (state < CANCELLED && state % 2 == 1); - } - - @Override - @Nullable - public Object scanUnsafe(Attr key) { - // touch guard - int state = this.state; - - if (key == Attr.TERMINATED) { - return (state < CANCELLED && state % 2 == 1); - } - if (key == Attr.PARENT) { - return subscription; - } - if (key == Attr.ERROR) { - return error; - } - if (key == Attr.PREFETCH) { - return Integer.MAX_VALUE; - } - if (key == Attr.CANCELLED) { - return state == CANCELLED; - } - return null; - } - - /** - * Return true if any {@link Subscriber} is actively subscribed - * - * @return true if any {@link Subscriber} is actively subscribed - */ - public final boolean hasDownstream() { - return state > NO_SUBSCRIBER_HAS_RESULT && hasDownstream; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/util/DisposableUtils.java b/rsocket-core/src/main/java/io/rsocket/util/DisposableUtils.java deleted file mode 100644 index c87a08220..000000000 --- a/rsocket-core/src/main/java/io/rsocket/util/DisposableUtils.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright 2015-2018 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.util; - -import java.util.Arrays; -import reactor.core.Disposable; - -/** Utilities for working with the {@link Disposable} type. */ -public final class DisposableUtils { - - private DisposableUtils() {} - - /** - * Calls the {@link Disposable#dispose()} method if the instance is not null. If any exceptions - * are thrown during disposal, suppress them. - * - * @param disposables the {@link Disposable}s to dispose - */ - public static void disposeQuietly(Disposable... disposables) { - Arrays.stream(disposables) - .forEach( - disposable -> { - try { - if (disposable != null) { - disposable.dispose(); - } - } catch (RuntimeException e) { - // Suppress any exceptions during disposal - } - }); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/util/DuplexConnectionProxy.java b/rsocket-core/src/main/java/io/rsocket/util/DuplexConnectionProxy.java deleted file mode 100644 index 2f5d1da4b..000000000 --- a/rsocket-core/src/main/java/io/rsocket/util/DuplexConnectionProxy.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright 2015-2019 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.util; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.rsocket.DuplexConnection; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public class DuplexConnectionProxy implements DuplexConnection { - private final DuplexConnection connection; - - public DuplexConnectionProxy(DuplexConnection connection) { - this.connection = connection; - } - - @Override - public Mono send(Publisher frames) { - return connection.send(frames); - } - - @Override - public Flux receive() { - return connection.receive(); - } - - @Override - public double availability() { - return connection.availability(); - } - - @Override - public ByteBufAllocator alloc() { - return connection.alloc(); - } - - @Override - public Mono onClose() { - return connection.onClose(); - } - - @Override - public void dispose() { - connection.dispose(); - } - - @Override - public boolean isDisposed() { - return connection.isDisposed(); - } - - public DuplexConnection delegate() { - return connection; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/util/Function3.java b/rsocket-core/src/main/java/io/rsocket/util/Function3.java deleted file mode 100644 index 5783665ae..000000000 --- a/rsocket-core/src/main/java/io/rsocket/util/Function3.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2015-2019 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.util; - -public interface Function3 { - - R apply(T t, U u, V v); -} diff --git a/rsocket-core/src/main/java/io/rsocket/util/MonoLifecycleHandler.java b/rsocket-core/src/main/java/io/rsocket/util/MonoLifecycleHandler.java deleted file mode 100644 index 4d47c03d6..000000000 --- a/rsocket-core/src/main/java/io/rsocket/util/MonoLifecycleHandler.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.rsocket.util; - -import javax.annotation.Nonnull; -import javax.annotation.Nullable; -import reactor.core.publisher.SignalType; - -public interface MonoLifecycleHandler { - - default void doOnSubscribe() {} - - /** - * Handler which is invoked on the terminal activity within a given Monoø - * - * @param signalType a type of signal which explain what happened - * @param element an carried element. May not be present if stream is empty or cancelled or - * errored - * @param e an carried error. May not be present if stream is cancelled or completed successfully - */ - default void doOnTerminal( - @Nonnull SignalType signalType, @Nullable T element, @Nullable Throwable e) {} -} diff --git a/rsocket-core/src/main/java/io/rsocket/util/MultiSubscriberRSocket.java b/rsocket-core/src/main/java/io/rsocket/util/MultiSubscriberRSocket.java deleted file mode 100644 index c2db6c238..000000000 --- a/rsocket-core/src/main/java/io/rsocket/util/MultiSubscriberRSocket.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright 2015-2019 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.util; - -import io.rsocket.Payload; -import io.rsocket.RSocket; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - -public class MultiSubscriberRSocket extends RSocketProxy { - public MultiSubscriberRSocket(RSocket source) { - super(source); - } - - @Override - public Mono fireAndForget(Payload payload) { - return Mono.defer(() -> super.fireAndForget(payload)); - } - - @Override - public Mono requestResponse(Payload payload) { - return Mono.defer(() -> super.requestResponse(payload)); - } - - @Override - public Flux requestStream(Payload payload) { - return Flux.defer(() -> super.requestStream(payload)); - } - - @Override - public Flux requestChannel(Publisher payloads) { - return Flux.defer(() -> super.requestChannel(payloads)); - } - - @Override - public Mono metadataPush(Payload payload) { - return Mono.defer(() -> super.metadataPush(payload)); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/util/OnceConsumer.java b/rsocket-core/src/main/java/io/rsocket/util/OnceConsumer.java deleted file mode 100644 index af4c038cc..000000000 --- a/rsocket-core/src/main/java/io/rsocket/util/OnceConsumer.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright 2015-2019 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.util; - -import java.util.function.Consumer; - -public abstract class OnceConsumer implements Consumer { - private boolean isFirst = true; - - @Override - public final void accept(T t) { - if (isFirst) { - isFirst = false; - acceptOnce(t); - } - } - - public abstract void acceptOnce(T t); -} diff --git a/rsocket-core/src/main/java/io/rsocket/util/RecyclerFactory.java b/rsocket-core/src/main/java/io/rsocket/util/RecyclerFactory.java deleted file mode 100644 index 30385195c..000000000 --- a/rsocket-core/src/main/java/io/rsocket/util/RecyclerFactory.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright 2015-2018 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.util; - -import io.netty.util.Recycler; -import io.netty.util.Recycler.Handle; -import java.util.Objects; -import java.util.function.Function; - -/** A factory for creating {@link Recycler}s. */ -public final class RecyclerFactory { - - /** - * Creates a new {@link Recycler}. - * - * @param newObjectCreator the {@link Function} to create a new object - * @param the type being recycled. - * @return the {@link Recycler} - * @throws NullPointerException if {@code newObjectCreator} is {@code null} - */ - public static Recycler createRecycler(Function, T> newObjectCreator) { - Objects.requireNonNull(newObjectCreator, "newObjectCreator must not be null"); - - return new Recycler() { - - @Override - protected T newObject(Handle handle) { - return newObjectCreator.apply(handle); - } - }; - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index 01cf99e26..fc87fc721 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -26,7 +26,6 @@ import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.DefaultPayload; -import io.rsocket.util.MultiSubscriberRSocket; import java.time.Duration; import java.util.Arrays; import java.util.Collection; @@ -74,23 +73,6 @@ void setUp() { RequesterLeaseHandler.None); } - @ParameterizedTest - @MethodSource("allInteractions") - void multiSubscriber(Function> interaction) { - RSocket multiSubsRSocket = new MultiSubscriberRSocket(rSocketRequester); - Flux response = Flux.from(interaction.apply(multiSubsRSocket)); - StepVerifier.withVirtualTime(() -> response.take(Duration.ofMillis(10))) - .thenAwait(Duration.ofMillis(10)) - .expectComplete() - .verify(Duration.ofSeconds(5)); - StepVerifier.withVirtualTime(() -> response.take(Duration.ofMillis(10))) - .thenAwait(Duration.ofMillis(10)) - .expectComplete() - .verify(Duration.ofSeconds(5)); - - Assertions.assertThat(requestFramesCount(connection.getSent())).isEqualTo(2); - } - @ParameterizedTest @MethodSource("allInteractions") void singleSubscriber(Function> interaction) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index 3b62bc437..b6067cdeb 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -25,12 +25,10 @@ import static io.rsocket.frame.FrameType.REQUEST_STREAM; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.contains; -import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.not; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -61,7 +59,6 @@ import io.rsocket.util.ByteBufPayload; import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; -import io.rsocket.util.MultiSubscriberRSocket; import java.time.Duration; import java.util.ArrayList; import java.util.Iterator; @@ -249,21 +246,6 @@ public void testRequestReplyErrorOnSend() { // verify(responseSub).onError(any(RuntimeException.class)); } - @Test - @Timeout(2_000) - public void testLazyRequestResponse() { - Publisher response = - new MultiSubscriberRSocket(rule.socket).requestResponse(EmptyPayload.INSTANCE); - int streamId = sendRequestResponse(response); - Assertions.assertThat(rule.connection.getSent()).hasSize(1).allMatch(ReferenceCounted::release); - rule.assertHasNoLeaks(); - rule.connection.clearSendReceiveBuffers(); - int streamId2 = sendRequestResponse(response); - assertThat("Stream ID reused.", streamId2, not(equalTo(streamId))); - Assertions.assertThat(rule.connection.getSent()).hasSize(1).allMatch(ReferenceCounted::release); - rule.assertHasNoLeaks(); - } - @Test @Timeout(2_000) public void testChannelRequestCancellation() { diff --git a/rsocket-core/src/test/java/io/rsocket/internal/LimitableRequestPublisherTest.java b/rsocket-core/src/test/java/io/rsocket/internal/LimitableRequestPublisherTest.java deleted file mode 100644 index 8c51c123e..000000000 --- a/rsocket-core/src/test/java/io/rsocket/internal/LimitableRequestPublisherTest.java +++ /dev/null @@ -1,33 +0,0 @@ -package io.rsocket.internal; - -import java.util.ArrayDeque; -import java.util.Queue; -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.RepeatedTest; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.DirectProcessor; -import reactor.test.util.RaceTestUtils; - -class LimitableRequestPublisherTest { - - @Test - @RepeatedTest(2) - public void requestLimitRacingTest() throws InterruptedException { - Queue requests = new ArrayDeque<>(10000); - LimitableRequestPublisher limitableRequestPublisher = - LimitableRequestPublisher.wrap(DirectProcessor.create().doOnRequest(requests::add), 0); - - Runnable request1 = () -> limitableRequestPublisher.request(1); - Runnable request2 = () -> limitableRequestPublisher.increaseInternalLimit(2); - - limitableRequestPublisher.subscribe(); - - for (int i = 0; i < 10000; i++) { - RaceTestUtils.race(request1, request2); - } - - Thread.sleep(1000); - - Assertions.assertThat(requests.stream().mapToLong(l -> l).sum()).isEqualTo(10000); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/internal/SwitchTransformFluxTest.java b/rsocket-core/src/test/java/io/rsocket/internal/SwitchTransformFluxTest.java deleted file mode 100644 index 07fbf695f..000000000 --- a/rsocket-core/src/test/java/io/rsocket/internal/SwitchTransformFluxTest.java +++ /dev/null @@ -1,446 +0,0 @@ -package io.rsocket.internal; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.hasItem; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; -import org.junit.Assert; -import org.junit.Assume; -import org.junit.Ignore; -import org.junit.Test; -import reactor.core.CoreSubscriber; -import reactor.core.publisher.Flux; -import reactor.core.scheduler.Schedulers; -import reactor.test.StepVerifier; -import reactor.test.publisher.TestPublisher; -import reactor.test.util.RaceTestUtils; -import reactor.util.context.Context; - -@Ignore -public class SwitchTransformFluxTest { - - @Test - public void shouldBeAbleToCancelSubscription() throws InterruptedException { - for (int j = 0; j < 10; j++) { - ArrayList capturedElements = new ArrayList<>(); - ArrayList capturedCompletions = new ArrayList<>(); - for (int i = 0; i < 1000; i++) { - TestPublisher publisher = TestPublisher.createCold(); - AtomicLong captureElement = new AtomicLong(0L); - AtomicBoolean captureCompletion = new AtomicBoolean(false); - AtomicLong requested = new AtomicLong(); - CountDownLatch latch = new CountDownLatch(1); - Flux switchTransformed = - publisher - .flux() - .doOnRequest(requested::addAndGet) - .doOnCancel(latch::countDown) - .transform( - flux -> new SwitchTransformFlux<>(flux, (first, innerFlux) -> innerFlux)); - - publisher.next(1L); - - switchTransformed.subscribe( - captureElement::set, - __ -> {}, - () -> captureCompletion.set(true), - s -> - new Thread( - () -> - RaceTestUtils.race( - publisher::complete, - () -> - RaceTestUtils.race( - s::cancel, () -> s.request(1), Schedulers.parallel()), - Schedulers.parallel())) - .start()); - - Assert.assertTrue(latch.await(5, TimeUnit.SECONDS)); - Assert.assertEquals(requested.get(), 1L); - capturedElements.add(captureElement.get()); - capturedCompletions.add(captureCompletion.get()); - } - - Assume.assumeThat(capturedElements, hasItem(equalTo(0L))); - Assume.assumeThat(capturedCompletions, hasItem(equalTo(false))); - } - } - - @Test - public void shouldRequestExpectedAmountOfElements() throws InterruptedException { - TestPublisher publisher = TestPublisher.createCold(); - AtomicLong capture = new AtomicLong(); - AtomicLong requested = new AtomicLong(); - CountDownLatch latch = new CountDownLatch(1); - Flux switchTransformed = - publisher - .flux() - .doOnRequest(requested::addAndGet) - .transform(flux -> new SwitchTransformFlux<>(flux, (first, innerFlux) -> innerFlux)); - - publisher.next(1L); - - switchTransformed.subscribe( - capture::set, - __ -> {}, - latch::countDown, - s -> { - for (int i = 0; i < 10000; i++) { - RaceTestUtils.race(() -> s.request(1), () -> s.request(1)); - } - RaceTestUtils.race(publisher::complete, publisher::complete); - }); - - latch.await(5, TimeUnit.SECONDS); - - Assert.assertEquals(capture.get(), 1L); - Assert.assertEquals(requested.get(), 20000L); - } - - @Test - public void shouldReturnCorrectContextOnEmptySource() { - Flux switchTransformed = - Flux.empty() - .transform(flux -> new SwitchTransformFlux<>(flux, (first, innerFlux) -> innerFlux)) - .subscriberContext(Context.of("a", "c")) - .subscriberContext(Context.of("c", "d")); - - StepVerifier.create(switchTransformed, 0) - .expectSubscription() - .thenRequest(1) - .expectAccessibleContext() - .contains("a", "c") - .contains("c", "d") - .then() - .expectComplete() - .verify(); - } - - @Test - public void shouldNotFailOnIncorrectPublisherBehavior() { - TestPublisher publisher = - TestPublisher.createNoncompliant(TestPublisher.Violation.CLEANUP_ON_TERMINATE); - Flux switchTransformed = - publisher - .flux() - .transform( - flux -> - new SwitchTransformFlux<>( - flux, - (first, innerFlux) -> innerFlux.subscriberContext(Context.of("a", "b")))); - - StepVerifier.create( - new Flux() { - @Override - public void subscribe(CoreSubscriber actual) { - switchTransformed.subscribe(actual); - publisher.next(1L); - } - }, - 0) - .thenRequest(1) - .expectNext(1L) - .thenRequest(1) - .then(() -> publisher.next(2L)) - .expectNext(2L) - .then(() -> publisher.error(new RuntimeException())) - .then(() -> publisher.error(new RuntimeException())) - .then(() -> publisher.error(new RuntimeException())) - .then(() -> publisher.error(new RuntimeException())) - .expectError() - .verifyThenAssertThat() - .hasDroppedErrors(3) - .tookLessThan(Duration.ofSeconds(10)); - - publisher.assertWasRequested(); - publisher.assertNoRequestOverflow(); - } - - // @Test - // public void shouldNotFailOnIncorrePu - - @Test - public void shouldBeAbleToAccessUpstreamContext() { - TestPublisher publisher = TestPublisher.createCold(); - - Flux switchTransformed = - publisher - .flux() - .transform( - flux -> - new SwitchTransformFlux<>( - flux, - (first, innerFlux) -> - innerFlux.map(String::valueOf).subscriberContext(Context.of("a", "b")))) - .subscriberContext(Context.of("a", "c")) - .subscriberContext(Context.of("c", "d")); - - publisher.next(1L); - - StepVerifier.create(switchTransformed, 0) - .thenRequest(1) - .expectNext("1") - .thenRequest(1) - .then(() -> publisher.next(2L)) - .expectNext("2") - .expectAccessibleContext() - .contains("a", "b") - .contains("c", "d") - .then() - .then(publisher::complete) - .expectComplete() - .verify(Duration.ofSeconds(10)); - - publisher.assertWasRequested(); - publisher.assertNoRequestOverflow(); - } - - @Test - public void shouldNotHangWhenOneElementUpstream() { - TestPublisher publisher = TestPublisher.createCold(); - - Flux switchTransformed = - publisher - .flux() - .transform( - flux -> - new SwitchTransformFlux<>( - flux, - (first, innerFlux) -> - innerFlux.map(String::valueOf).subscriberContext(Context.of("a", "b")))) - .subscriberContext(Context.of("a", "c")) - .subscriberContext(Context.of("c", "d")); - - publisher.next(1L); - publisher.complete(); - - StepVerifier.create(switchTransformed, 0) - .thenRequest(1) - .expectNext("1") - .expectComplete() - .verify(Duration.ofSeconds(10)); - - publisher.assertWasRequested(); - publisher.assertNoRequestOverflow(); - } - - @Test - public void backpressureTest() { - TestPublisher publisher = TestPublisher.createCold(); - AtomicLong requested = new AtomicLong(); - - Flux switchTransformed = - publisher - .flux() - .doOnRequest(requested::addAndGet) - .transform( - flux -> - new SwitchTransformFlux<>( - flux, (first, innerFlux) -> innerFlux.map(String::valueOf))); - - publisher.next(1L); - - StepVerifier.create(switchTransformed, 0) - .thenRequest(1) - .expectNext("1") - .thenRequest(1) - .then(() -> publisher.next(2L)) - .expectNext("2") - .then(publisher::complete) - .expectComplete() - .verify(Duration.ofSeconds(10)); - - publisher.assertWasRequested(); - publisher.assertNoRequestOverflow(); - - Assert.assertEquals(2L, requested.get()); - } - - @Test - public void backpressureConditionalTest() { - Flux publisher = Flux.range(0, 10000); - AtomicLong requested = new AtomicLong(); - - Flux switchTransformed = - publisher - .doOnRequest(requested::addAndGet) - .transform( - flux -> - new SwitchTransformFlux<>( - flux, (first, innerFlux) -> innerFlux.map(String::valueOf))) - .filter(e -> false); - - StepVerifier.create(switchTransformed, 0) - .thenRequest(1) - .expectComplete() - .verify(Duration.ofSeconds(10)); - - Assert.assertEquals(2L, requested.get()); - } - - @Test - public void backpressureHiddenConditionalTest() { - Flux publisher = Flux.range(0, 10000); - AtomicLong requested = new AtomicLong(); - - Flux switchTransformed = - publisher - .doOnRequest(requested::addAndGet) - .transform( - flux -> - new SwitchTransformFlux<>( - flux, (first, innerFlux) -> innerFlux.map(String::valueOf).hide())) - .filter(e -> false); - - StepVerifier.create(switchTransformed, 0) - .thenRequest(1) - .expectComplete() - .verify(Duration.ofSeconds(10)); - - Assert.assertEquals(10001L, requested.get()); - } - - @Test - public void backpressureDrawbackOnConditionalInTransformTest() { - Flux publisher = Flux.range(0, 10000); - AtomicLong requested = new AtomicLong(); - - Flux switchTransformed = - publisher - .doOnRequest(requested::addAndGet) - .transform( - flux -> - new SwitchTransformFlux<>( - flux, - (first, innerFlux) -> innerFlux.map(String::valueOf).filter(e -> false))); - - StepVerifier.create(switchTransformed, 0) - .thenRequest(1) - .expectComplete() - .verify(Duration.ofSeconds(10)); - - Assert.assertEquals(10001L, requested.get()); - } - - @Test - public void shouldErrorOnOverflowTest() { - TestPublisher publisher = TestPublisher.createCold(); - - Flux switchTransformed = - publisher - .flux() - .transform( - flux -> - new SwitchTransformFlux<>( - flux, (first, innerFlux) -> innerFlux.map(String::valueOf))); - - publisher.next(1L); - - StepVerifier.create(switchTransformed, 0) - .thenRequest(1) - .expectNext("1") - .then(() -> publisher.next(2L)) - .expectError() - .verify(Duration.ofSeconds(10)); - - publisher.assertWasRequested(); - publisher.assertNoRequestOverflow(); - } - - @Test - public void shouldPropagateonCompleteCorrectly() { - Flux switchTransformed = - Flux.empty() - .transform( - flux -> - new SwitchTransformFlux<>( - flux, (first, innerFlux) -> innerFlux.map(String::valueOf))); - - StepVerifier.create(switchTransformed).expectComplete().verify(Duration.ofSeconds(10)); - } - - @Test - public void shouldPropagateErrorCorrectly() { - Flux switchTransformed = - Flux.error(new RuntimeException("hello")) - .transform( - flux -> - new SwitchTransformFlux<>( - flux, (first, innerFlux) -> innerFlux.map(String::valueOf))); - - StepVerifier.create(switchTransformed) - .expectErrorMessage("hello") - .verify(Duration.ofSeconds(10)); - } - - @Test - public void shouldBeAbleToBeCancelledProperly() { - TestPublisher publisher = TestPublisher.createCold(); - Flux switchTransformed = - publisher - .flux() - .transform( - flux -> - new SwitchTransformFlux<>( - flux, (first, innerFlux) -> innerFlux.map(String::valueOf))); - - publisher.next(1); - - StepVerifier.create(switchTransformed, 0).thenCancel().verify(Duration.ofSeconds(10)); - - publisher.assertCancelled(); - publisher.assertWasRequested(); - } - - @Test - public void shouldBeAbleToCatchDiscardedElement() { - TestPublisher publisher = TestPublisher.createCold(); - Integer[] discarded = new Integer[1]; - Flux switchTransformed = - publisher - .flux() - .transform( - flux -> - new SwitchTransformFlux<>( - flux, (first, innerFlux) -> innerFlux.map(String::valueOf))) - .doOnDiscard(Integer.class, e -> discarded[0] = e); - - publisher.next(1); - - StepVerifier.create(switchTransformed, 0).thenCancel().verify(Duration.ofSeconds(10)); - - publisher.assertCancelled(); - publisher.assertWasRequested(); - - Assert.assertArrayEquals(new Integer[] {1}, discarded); - } - - @Test - public void shouldBeAbleToCatchDiscardedElementInCaseOfConditional() { - TestPublisher publisher = TestPublisher.createCold(); - Integer[] discarded = new Integer[1]; - Flux switchTransformed = - publisher - .flux() - .transform( - flux -> - new SwitchTransformFlux<>( - flux, (first, innerFlux) -> innerFlux.map(String::valueOf))) - .filter(t -> true) - .doOnDiscard(Integer.class, e -> discarded[0] = e); - - publisher.next(1); - - StepVerifier.create(switchTransformed, 0).thenCancel().verify(Duration.ofSeconds(10)); - - publisher.assertCancelled(); - publisher.assertWasRequested(); - - Assert.assertArrayEquals(new Integer[] {1}, discarded); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoEmptyTest.java b/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoEmptyTest.java deleted file mode 100644 index 76bb953a4..000000000 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoEmptyTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package io.rsocket.internal; - -import static io.rsocket.internal.SchedulerUtils.warmup; - -import java.time.Duration; -import java.util.concurrent.atomic.AtomicInteger; -import org.assertj.core.api.Assertions; -import org.junit.Test; -import reactor.core.publisher.Mono; -import reactor.core.scheduler.Schedulers; -import reactor.test.util.RaceTestUtils; - -public class UnicastMonoEmptyTest { - - @Test - public void shouldSupportASingleSubscriber() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - AtomicInteger times = new AtomicInteger(); - Mono unicastMono = UnicastMonoEmpty.newInstance(times::incrementAndGet); - - Assertions.assertThatThrownBy( - () -> - RaceTestUtils.race( - unicastMono::subscribe, unicastMono::subscribe, Schedulers.single())) - .hasCause(new IllegalStateException("UnicastMonoEmpty allows only a single Subscriber")); - Assertions.assertThat(times.get()).isEqualTo(1); - } - } - - @Test - public void shouldSupportASingleBlock() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - AtomicInteger times = new AtomicInteger(); - Mono unicastMono = UnicastMonoEmpty.newInstance(times::incrementAndGet); - - Assertions.assertThatThrownBy( - () -> RaceTestUtils.race(unicastMono::block, unicastMono::block, Schedulers.single())) - .hasMessage("UnicastMonoEmpty allows only a single Subscriber"); - Assertions.assertThat(times.get()).isEqualTo(1); - } - } - - @Test - public void shouldSupportASingleBlockWithTimeout() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - AtomicInteger times = new AtomicInteger(); - Mono unicastMono = UnicastMonoEmpty.newInstance(times::incrementAndGet); - - Assertions.assertThatThrownBy( - () -> - RaceTestUtils.race( - () -> unicastMono.block(Duration.ofMinutes(1)), - () -> unicastMono.block(Duration.ofMinutes(1)), - Schedulers.single())) - .hasMessage("UnicastMonoEmpty allows only a single Subscriber"); - Assertions.assertThat(times.get()).isEqualTo(1); - } - } - - @Test - public void shouldSupportASingleSubscribeOrBlock() throws InterruptedException { - warmup(Schedulers.parallel()); - - for (int i = 0; i < 10000; i++) { - AtomicInteger times = new AtomicInteger(); - Mono unicastMono = UnicastMonoEmpty.newInstance(times::incrementAndGet); - - Assertions.assertThatThrownBy( - () -> - RaceTestUtils.race( - unicastMono::subscribe, - () -> - RaceTestUtils.race( - unicastMono::block, - () -> unicastMono.block(Duration.ofMinutes(1)), - Schedulers.parallel()), - Schedulers.parallel())) - .matches( - t -> { - Assertions.assertThat(t.getSuppressed()).hasSize(2); - Assertions.assertThat(t.getSuppressed()[0]) - .hasMessageContaining("UnicastMonoEmpty allows only a single Subscriber"); - Assertions.assertThat(t.getSuppressed()[1]) - .hasMessageContaining("UnicastMonoEmpty allows only a single Subscriber"); - - return true; - }); - Assertions.assertThat(times.get()).isEqualTo(1); - } - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java b/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java deleted file mode 100644 index a836dd509..000000000 --- a/rsocket-core/src/test/java/io/rsocket/internal/UnicastMonoProcessorTest.java +++ /dev/null @@ -1,1780 +0,0 @@ -/* - * Copyright 2015-2018 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.internal; - -import static io.rsocket.internal.SchedulerUtils.warmup; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -import io.rsocket.internal.subscriber.AssertSubscriber; -import io.rsocket.util.MonoLifecycleHandler; -import java.lang.ref.WeakReference; -import java.time.Duration; -import java.util.ArrayList; -import java.util.Date; -import java.util.concurrent.CancellationException; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; -import org.assertj.core.api.Assertions; -import org.junit.Test; -import org.reactivestreams.Subscriber; -import org.reactivestreams.Subscription; -import reactor.core.Scannable; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Hooks; -import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoProcessor; -import reactor.core.publisher.Operators; -import reactor.core.publisher.SignalType; -import reactor.core.scheduler.Schedulers; -import reactor.test.StepVerifier; -import reactor.test.publisher.TestPublisher; -import reactor.test.util.RaceTestUtils; -import reactor.util.context.Context; -import reactor.util.function.Tuple2; - -public class UnicastMonoProcessorTest { - - static class VerifyMonoLifecycleHandler implements MonoLifecycleHandler { - private final AtomicInteger onSubscribeCounter = new AtomicInteger(); - private final AtomicInteger onTerminalCounter = new AtomicInteger(); - private final AtomicReference valueReference = new AtomicReference<>(); - private final AtomicReference errorReference = new AtomicReference<>(); - private final AtomicReference signalTypeReference = new AtomicReference<>(); - - @Override - public void doOnSubscribe() { - onSubscribeCounter.incrementAndGet(); - } - - @Override - public void doOnTerminal(SignalType signalType, T element, Throwable e) { - onTerminalCounter.incrementAndGet(); - signalTypeReference.set(signalType); - valueReference.set(element); - errorReference.set(e); - } - - public VerifyMonoLifecycleHandler assertSubscribed() { - assertThat(onSubscribeCounter.get()).isOne(); - return this; - } - - public VerifyMonoLifecycleHandler assertNotSubscribed() { - assertThat(onSubscribeCounter.get()).isZero(); - return this; - } - - public VerifyMonoLifecycleHandler assertTerminated() { - assertThat(onTerminalCounter.get()).describedAs("Expected a single terminal signal").isOne(); - return this; - } - - public VerifyMonoLifecycleHandler assertNotTerminated() { - assertThat(onTerminalCounter.get()).describedAs("Expected zero terminal signals").isZero(); - return this; - } - - public VerifyMonoLifecycleHandler assertCompleted() { - assertTerminated(); - assertThat(signalTypeReference.get()) - .describedAs("Expected ON_COMPLETE signal") - .isEqualTo(SignalType.ON_COMPLETE); - assertThat(errorReference.get()).describedAs("Expected error to be absent").isNull(); - assertThat(valueReference.get()).isNull(); - return this; - } - - public VerifyMonoLifecycleHandler assertCompleted(T value) { - assertTerminated(); - assertThat(signalTypeReference.get()) - .describedAs("Expected ON_COMPLETE signal") - .isEqualTo(SignalType.ON_COMPLETE); - assertThat(errorReference.get()).describedAs("Expected error to be absent").isNull(); - assertThat(valueReference.get()).isEqualTo(value); - return this; - } - - public VerifyMonoLifecycleHandler assertErrored() { - assertTerminated(); - assertThat(signalTypeReference.get()) - .describedAs("Expected ON_ERROR signal") - .isEqualTo(SignalType.ON_ERROR); - assertThat(errorReference.get()).describedAs("Expected error to be present").isNotNull(); - assertThat(valueReference.get()).isNull(); - return this; - } - - public VerifyMonoLifecycleHandler assertCancelled() { - assertTerminated(); - assertThat(signalTypeReference.get()) - .describedAs("Expected ON_ERROR signal") - .isEqualTo(SignalType.CANCEL); - assertThat(errorReference.get()).describedAs("Expected error to be absent").isNull(); - assertThat(valueReference.get()).isNull(); - return this; - } - } - - @Test - public void testUnicast() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - verifyMonoLifecycleHandler.assertNotSubscribed(); - assertThatThrownBy(() -> RaceTestUtils.race(processor::subscribe, processor::subscribe)) - .hasCause( - new IllegalStateException("UnicastMonoProcessor allows only a single Subscriber")); - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - } - } - - @Test - public void stateFlowTest1_Next() { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = AssertSubscriber.create(0); - - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.onNext(1); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoEvents(); - assertSubscriber.request(1); - - verifyMonoLifecycleHandler.assertCompleted(1); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertValues(1); - assertSubscriber.assertComplete(); - } - - @Test - public void stateFlowTest1_Complete() { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = AssertSubscriber.create(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.onComplete(); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoValues(); - assertSubscriber.assertComplete(); - } - - @Test - public void stateFlowTest1_Error() { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = AssertSubscriber.create(0); - RuntimeException testError = new RuntimeException("test"); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.onError(testError); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoValues(); - assertSubscriber.assertError(RuntimeException.class); - assertSubscriber.assertErrorMessage("test"); - } - - @Test - public void stateFlowTest1_Dispose() { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = AssertSubscriber.create(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.dispose(); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoValues(); - assertSubscriber.assertError(CancellationException.class); - assertSubscriber.assertErrorMessage("Disposed"); - } - - @Test - public void stateFlowTest2_Next() { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = AssertSubscriber.create(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - processor.onNext(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoEvents(); - assertSubscriber.request(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertValues(1); - assertSubscriber.assertComplete(); - } - - @Test - public void stateFlowTest2_Complete() { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = AssertSubscriber.create(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - processor.onComplete(); - - verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoValues(); - assertSubscriber.assertComplete(); - } - - @Test - public void stateFlowTest2_Error() { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = AssertSubscriber.create(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - processor.onError(new RuntimeException("Test")); - - verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoValues(); - assertSubscriber.assertError(RuntimeException.class); - assertSubscriber.assertErrorMessage("Test"); - } - - @Test - public void stateFlowTest2_Dispose() { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = AssertSubscriber.create(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - processor.dispose(); - - verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoValues(); - assertSubscriber.assertError(CancellationException.class); - assertSubscriber.assertErrorMessage("Disposed"); - } - - @Test - public void stateFlowTest3_Next() { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = AssertSubscriber.create(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - assertSubscriber.assertNoEvents(); - assertSubscriber.request(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); - - assertSubscriber.assertNoEvents(); - processor.onNext(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertValues(1); - assertSubscriber.assertComplete(); - } - - @Test - public void stateFlowTest3_Complete() { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = AssertSubscriber.create(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - assertSubscriber.request(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); - - processor.onComplete(); - - verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoValues(); - assertSubscriber.assertComplete(); - } - - @Test - public void stateFlowTest3_Error() { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = AssertSubscriber.create(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - assertSubscriber.request(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); - - processor.onError(new RuntimeException("Test")); - - verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoValues(); - assertSubscriber.assertError(RuntimeException.class); - assertSubscriber.assertErrorMessage("Test"); - } - - @Test - public void stateFlowTest3_Dispose() { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = AssertSubscriber.create(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - assertSubscriber.request(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); - - processor.dispose(); - - verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoValues(); - assertSubscriber.assertError(RuntimeException.class); - assertSubscriber.assertErrorMessage("Disposed"); - } - - @Test - public void stateFlowTest4_Next() { - ArrayList discarded = new ArrayList<>(); - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - // Context discardingContext = Operators.enableOnDiscard(null, discarded::add); - Hooks.onNextDropped(discarded::add); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); - - try { - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - assertSubscriber.request(1); - assertSubscriber.assertNoEvents(); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); - - assertSubscriber.cancel(); - assertSubscriber.assertNoEvents(); - - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - processor.onNext(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - assertSubscriber.assertNoEvents(); - assertThat(discarded).containsExactly(1); - } finally { - Hooks.resetOnNextDropped(); - } - } - - @Test - public void stateFlowTest4_Error() { - ArrayList discarded = new ArrayList<>(); - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - // Context discardingContext = Operators.enableOnDiscard(null, discarded::add); - Hooks.onErrorDropped(discarded::add); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); - - try { - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - assertSubscriber.request(1); - assertSubscriber.assertNoEvents(); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); - - assertSubscriber.cancel(); - assertSubscriber.assertNoEvents(); - - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - RuntimeException testError = new RuntimeException("test"); - processor.onError(testError); - - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - assertSubscriber.assertNoEvents(); - assertThat(discarded).containsExactly(testError); - - } finally { - Hooks.resetOnErrorDropped(); - } - } - - @Test - public void stateFlowTest4_Dispose() { - ArrayList discarded = new ArrayList<>(); - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - // Context discardingContext = Operators.enableOnDiscard(null, discarded::add); - Hooks.onErrorDropped(discarded::add); - try { - AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - assertSubscriber.request(1); - assertSubscriber.assertNoEvents(); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); - - assertSubscriber.cancel(); - assertSubscriber.assertNoEvents(); - - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - processor.dispose(); - - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - assertSubscriber.assertNoEvents(); - assertThat(discarded).isEmpty(); - } finally { - Hooks.resetOnErrorDropped(); - } - } - - @Test - public void stateFlowTest4_Complete() { - ArrayList discarded = new ArrayList<>(); - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - // Context discardingContext = Operators.enableOnDiscard(null, discarded::add); - Hooks.onErrorDropped(discarded::add); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); - - try { - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - assertSubscriber.request(1); - assertSubscriber.assertNoEvents(); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); - - assertSubscriber.cancel(); - assertSubscriber.assertNoEvents(); - - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - processor.onComplete(); - - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - assertSubscriber.assertNoEvents(); - assertThat(discarded).isEmpty(); - } finally { - Hooks.resetOnErrorDropped(); - } - } - - @Test - public void stateFlowTest5_Next() { - ArrayList discarded = new ArrayList<>(); - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - Context discardingContext = Operators.enableOnDiscard(null, discarded::add); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - processor.onNext(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_HAS_RESULT); - - assertSubscriber.cancel(); - - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - assertSubscriber.request(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - assertSubscriber.assertNoEvents(); - assertThat(discarded).containsExactly(1); - } - - @Test - public void stateFlowTest5_Complete() { - ArrayList discarded = new ArrayList<>(); - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - Context discardingContext = Operators.enableOnDiscard(null, discarded::add); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - processor.onComplete(); - - verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.cancel(); - - verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - assertSubscriber.assertComplete(); - assertThat(discarded).isEmpty(); - } - - @Test - public void stateFlowTest5_Error() { - ArrayList discarded = new ArrayList<>(); - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - Context discardingContext = Operators.enableOnDiscard(null, discarded::add); - Hooks.onErrorDropped(discarded::add); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); - - try { - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - processor.onError(new RuntimeException("test")); - - verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.cancel(); - - verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - assertSubscriber.assertError(RuntimeException.class); - assertSubscriber.assertErrorMessage("test"); - assertThat(discarded).isEmpty(); - } finally { - Hooks.resetOnErrorDropped(); - } - } - - @Test - public void stateFlowTest5_Dispose() { - ArrayList discarded = new ArrayList<>(); - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - Context discardingContext = Operators.enableOnDiscard(null, discarded::add); - Hooks.onErrorDropped(discarded::add); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); - - try { - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_NO_RESULT); - - processor.dispose(); - - verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.cancel(); - - verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - assertSubscriber.assertError(CancellationException.class); - assertSubscriber.assertErrorMessage("Disposed"); - assertThat(discarded).isEmpty(); - } finally { - Hooks.resetOnErrorDropped(); - } - } - - @Test - public void stateFlowTest6_Next() { - ArrayList discarded = new ArrayList<>(); - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - Context discardingContext = Operators.enableOnDiscard(null, discarded::add); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(discardingContext, 0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.onNext(1); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_HAS_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_REQUEST_HAS_RESULT); - - assertSubscriber.cancel(); - - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - assertSubscriber.request(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - assertSubscriber.assertNoEvents(); - assertThat(discarded).containsExactly(1); - } - - @Test - public void stateFlowTest7_Next() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - RaceTestUtils.race( - () -> processor.onNext(1), - () -> processor.subscribe(assertSubscriber), - Schedulers.single()); - - verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertValues(1); - assertSubscriber.assertComplete(); - } - } - - @Test - public void stateFlowTest7_Complete() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); - - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - RaceTestUtils.race( - processor::onComplete, () -> processor.subscribe(assertSubscriber), Schedulers.single()); - - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoValues(); - assertSubscriber.assertComplete(); - } - } - - @Test - public void stateFlowTest7_Error() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); - - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - RaceTestUtils.race( - () -> processor.onError(new RuntimeException("test")), - () -> processor.subscribe(assertSubscriber), - Schedulers.single()); - - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoValues(); - assertSubscriber.assertError(RuntimeException.class); - assertSubscriber.assertErrorMessage("test"); - } - } - - @Test - public void stateFlowTest7_Dispose() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - RaceTestUtils.race( - processor::dispose, () -> processor.subscribe(assertSubscriber), Schedulers.single()); - - verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertNoValues(); - assertSubscriber.assertError(CancellationException.class); - assertSubscriber.assertErrorMessage("Disposed"); - } - } - - @Test - public void stateFlowTest8_Next() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); - - RaceTestUtils.race(() -> processor.onNext(1), assertSubscriber::cancel, Schedulers.single()); - - verifyMonoLifecycleHandler.assertSubscribed().assertTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - if (assertSubscriber.values().isEmpty()) { - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertSubscriber.assertNoEvents(); - } else { - verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); - assertSubscriber.assertValues(1); - assertSubscriber.assertComplete(); - } - } - } - - @Test - public void stateFlowTest9_Next() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_NO_RESULT); - - RaceTestUtils.race(() -> processor.onNext(1), processor::dispose, Schedulers.single()); - - verifyMonoLifecycleHandler.assertSubscribed().assertTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - if (processor.isError()) { - verifyMonoLifecycleHandler.assertSubscribed().assertErrored(); - assertSubscriber.assertNoValues(); - assertSubscriber.assertErrorMessage("Disposed"); - } else { - verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); - assertSubscriber.assertValues(1); - assertSubscriber.assertComplete(); - } - } - } - - @Test - public void stateFlowTest13_Next() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.onNext(1); - processor.subscribe(assertSubscriber); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - - RaceTestUtils.race( - () -> assertSubscriber.request(1), - () -> assertSubscriber.request(1), - Schedulers.single()); - - verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertValues(1); - assertSubscriber.assertComplete(); - } - } - - @Test - public void stateFlowTest14_Next() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - assertSubscriber.request(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - RaceTestUtils.race(() -> processor.onNext(1), () -> processor.onNext(1), Schedulers.single()); - - verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.HAS_REQUEST_HAS_RESULT); - - assertSubscriber.assertValues(1); - assertSubscriber.assertComplete(); - } - } - - @Test - public void stateFlowTest15_Next() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - processor.onNext(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - RaceTestUtils.race( - () -> assertSubscriber.request(1), assertSubscriber::cancel, Schedulers.single()); - - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - if (assertSubscriber.values().isEmpty()) { - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertSubscriber.assertNoEvents(); - } else { - verifyMonoLifecycleHandler.assertSubscribed().assertCompleted(1); - assertSubscriber.assertValues(1); - assertSubscriber.assertComplete(); - } - } - } - - @Test - public void stateFlowTest16_Next() throws InterruptedException { - warmup(Schedulers.single()); - - for (int i = 0; i < 10000; i++) { - VerifyMonoLifecycleHandler verifyMonoLifecycleHandler = - new VerifyMonoLifecycleHandler<>(); - UnicastMonoProcessor processor = - UnicastMonoProcessor.create(verifyMonoLifecycleHandler); - AssertSubscriber assertSubscriber = new AssertSubscriber<>(0); - - verifyMonoLifecycleHandler.assertNotSubscribed().assertNotTerminated(); - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.NO_SUBSCRIBER_NO_RESULT); - - processor.subscribe(assertSubscriber); - processor.onNext(1); - - verifyMonoLifecycleHandler.assertSubscribed().assertNotTerminated(); - RaceTestUtils.race(assertSubscriber::cancel, assertSubscriber::cancel, Schedulers.single()); - - assertThat(processor.state).isEqualTo(UnicastMonoProcessor.CANCELLED); - - verifyMonoLifecycleHandler.assertSubscribed().assertCancelled(); - assertSubscriber.assertNoEvents(); - } - } - - @Test - public void noRetentionOnTermination() throws InterruptedException { - Date date = new Date(); - CompletableFuture future = new CompletableFuture<>(); - - WeakReference refDate = new WeakReference<>(date); - WeakReference> refFuture = new WeakReference<>(future); - - Mono source = Mono.fromFuture(future); - Mono data = - source.map(Date::toString).log().subscribeWith(UnicastMonoProcessor.create()).log(); - - future.complete(date); - assertThat(data.block()).isEqualTo(date.toString()); - - date = null; - future = null; - source = null; - System.gc(); - - int cycles; - for (cycles = 10; cycles > 0; cycles--) { - if (refDate.get() == null && refFuture.get() == null) break; - Thread.sleep(100); - } - - assertThat(refFuture.get()).isNull(); - assertThat(refDate.get()).isNull(); - assertThat(cycles).isNotZero().isPositive(); - } - - @Test - public void noRetentionOnTerminationError() throws InterruptedException { - CompletableFuture future = new CompletableFuture<>(); - - WeakReference> refFuture = new WeakReference<>(future); - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); - - Mono source = Mono.fromFuture(future); - Mono data = source.map(Date::toString).subscribeWith(processor); - - future.completeExceptionally(new IllegalStateException()); - - assertThatExceptionOfType(IllegalStateException.class).isThrownBy(data::block); - - future = null; - source = null; - System.gc(); - - int cycles; - for (cycles = 10; cycles > 0; cycles--) { - if (refFuture.get() == null) break; - Thread.sleep(100); - } - - assertThat(refFuture.get()).isNull(); - assertThat(cycles).isNotZero().isPositive(); - } - - @Test - public void noRetentionOnTerminationCancel() throws InterruptedException { - CompletableFuture future = new CompletableFuture<>(); - - WeakReference> refFuture = new WeakReference<>(future); - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); - - Mono source = Mono.fromFuture(future); - Mono data = - source.map(Date::toString).transformDeferred((s) -> s.subscribeWith(processor)); - - future = null; - source = null; - - data.subscribe().dispose(); - processor.dispose(); - - data = null; - - System.gc(); - - int cycles; - for (cycles = 10; cycles > 0; cycles--) { - if (refFuture.get() == null) break; - Thread.sleep(100); - } - - assertThat(refFuture.get()).isNull(); - assertThat(cycles).isNotZero().isPositive(); - } - - @Test(expected = IllegalStateException.class) - public void MonoProcessorResultNotAvailable() { - MonoProcessor mp = MonoProcessor.create(); - mp.block(Duration.ofMillis(1)); - } - - @Test - public void MonoProcessorRejectedDoOnSuccessOrError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - mp.doOnSuccessOrError((s, f) -> ref.set(f)).subscribe(); - mp.onError(new Exception("test")); - - assertThat(ref.get()).hasMessage("test"); - assertThat(mp.isError()).isTrue(); - } - - @Test - public void MonoProcessorRejectedDoOnTerminate() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicInteger invoked = new AtomicInteger(); - - mp.doOnTerminate(invoked::incrementAndGet).subscribe(); - mp.onError(new Exception("test")); - - assertThat(invoked.get()).isEqualTo(1); - assertThat(mp.isError()).isTrue(); - } - - @Test - public void MonoProcessorRejectedSubscribeCallback() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - mp.subscribe(v -> {}, ref::set); - mp.onError(new Exception("test")); - - assertThat(ref.get()).hasMessage("test"); - assertThat(mp.isError()).isTrue(); - } - - @Test - public void MonoProcessorSuccessDoOnSuccessOrError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - mp.doOnSuccessOrError((s, f) -> ref.set(s)).subscribe(); - mp.onNext("test"); - - assertThat(ref.get()).isEqualToIgnoringCase("test"); - assertThat(mp.isDisposed()).isTrue(); - assertThat(mp.isError()).isFalse(); - } - - @Test - public void MonoProcessorSuccessDoOnTerminate() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicInteger invoked = new AtomicInteger(); - - mp.doOnTerminate(invoked::incrementAndGet).subscribe(); - mp.onNext("test"); - - assertThat(invoked.get()).isEqualTo(1); - assertThat(mp.isDisposed()).isTrue(); - assertThat(mp.isError()).isFalse(); - } - - @Test - public void MonoProcessorSuccessSubscribeCallback() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - mp.subscribe(ref::set); - mp.onNext("test"); - - assertThat(ref.get()).isEqualToIgnoringCase("test"); - assertThat(mp.isDisposed()).isTrue(); - assertThat(mp.isError()).isFalse(); - } - - @Test - public void MonoProcessorRejectedDoOnError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - mp.doOnError(ref::set).subscribe(); - mp.onError(new Exception("test")); - - assertThat(ref.get()).hasMessage("test"); - assertThat(mp.isError()).isTrue(); - } - - @Test(expected = NullPointerException.class) - public void MonoProcessorRejectedSubscribeCallbackNull() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.subscribe((Subscriber) null); - } - - @Test - public void MonoProcessorSuccessDoOnSuccess() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - mp.doOnSuccess(ref::set).subscribe(); - mp.onNext("test"); - - assertThat(ref.get()).isEqualToIgnoringCase("test"); - assertThat(mp.isDisposed()).isTrue(); - assertThat(mp.isError()).isFalse(); - } - - @Test - public void MonoProcessorSuccessChainTogether() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - mp.subscribe(mp2); - - mp.onNext("test"); - - assertThat(mp2.peek()).isEqualToIgnoringCase("test"); - assertThat(mp.isDisposed()).isTrue(); - assertThat(mp.isError()).isFalse(); - } - - @Test - public void MonoProcessorRejectedChainTogether() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - mp.subscribe(mp2); - - mp.onError(new Exception("test")); - - assertThat(mp2.getError()).hasMessage("test"); - assertThat(mp.isError()).isTrue(); - } - - @Test - public void MonoProcessorDoubleFulfill() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - StepVerifier.create(mp) - .then( - () -> { - mp.onNext("test1"); - mp.onNext("test2"); - }) - .expectNext("test1") - .expectComplete() - .verifyThenAssertThat() - .hasDroppedExactly("test2"); - } - - @Test - public void MonoProcessorNullFulfill() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.onNext(null); - - assertThat(mp.isDisposed()).isTrue(); - assertThat(mp.peek()).isNull(); - } - - @Test - public void MonoProcessorMapFulfill() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.onNext(1); - - UnicastMonoProcessor mp2 = - mp.map(s -> s * 2).subscribeWith(UnicastMonoProcessor.create()); - - assertThat(mp2.isDisposed()).isTrue(); - assertThat(mp2.isTerminated()).isTrue(); - assertThat(mp2.isCancelled()).isFalse(); - assertThat(mp2.peek()).isEqualTo(2); - - mp2.subscribe(); - } - - @Test - public void MonoProcessorThenFulfill() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.onNext(1); - - UnicastMonoProcessor mp2 = - mp.flatMap(s -> Mono.just(s * 2)).subscribeWith(UnicastMonoProcessor.create()); - - assertThat(mp2.isDisposed()).isTrue(); - assertThat(mp2.isTerminated()).isTrue(); - assertThat(mp2.isCancelled()).isFalse(); - assertThat(mp2.peek()).isEqualTo(2); - - mp2.subscribe(); - } - - @Test - public void MonoProcessorMapError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.onNext(1); - - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - - StepVerifier.create( - mp.map( - s -> { - throw new RuntimeException("test"); - }) - .subscribeWith(mp2), - 0) - .thenRequest(1) - .then( - () -> { - assertThat(mp2.isDisposed()).isTrue(); - assertThat(mp2.getError()).hasMessage("test"); - }) - .verifyErrorMessage("test"); - } - - @Test(expected = Exception.class) - public void MonoProcessorDoubleError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.onError(new Exception("test")); - mp.onError(new Exception("test")); - } - - @Test(expected = Exception.class) - public void MonoProcessorDoubleSignal() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - - mp.onNext("test"); - mp.onError(new Exception("test")); - } - - @Test - public void zipMonoProcessor() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - UnicastMonoProcessor> mp3 = UnicastMonoProcessor.create(); - - StepVerifier.create(Mono.zip(mp, mp2).subscribeWith(mp3), 0) - .then(() -> assertThat(mp3.isDisposed()).isFalse()) - .then(() -> mp.onNext(1)) - .then(() -> assertThat(mp3.isDisposed()).isFalse()) - .then(() -> mp2.onNext(2)) - .then( - () -> { - assertThat(mp3.isDisposed()).isTrue(); - assertThat(mp3.peek().getT1()).isEqualTo(1); - assertThat(mp3.peek().getT2()).isEqualTo(2); - }) - .thenRequest(1) - .expectNextMatches(t -> t.getT1() == 1 && t.getT2() == 2) - .verifyComplete(); - } - - @Test - public void zipMonoProcessor2() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp3 = UnicastMonoProcessor.create(); - - StepVerifier.create(Mono.zip(d -> (Integer) d[0], mp).subscribeWith(mp3), 0) - .then(() -> assertThat(mp3.isDisposed()).isFalse()) - .then(() -> mp.onNext(1)) - .then( - () -> { - assertThat(mp3.isDisposed()).isTrue(); - assertThat(mp3.peek()).isEqualTo(1); - }) - .thenRequest(1) - .expectNext(1) - .verifyComplete(); - } - - @Test - public void zipMonoProcessorRejected() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - UnicastMonoProcessor> mp3 = UnicastMonoProcessor.create(); - - StepVerifier.create(Mono.zip(mp, mp2).subscribeWith(mp3)) - .then(() -> assertThat(mp3.isDisposed()).isFalse()) - .then(() -> mp.onError(new Exception("test"))) - .then( - () -> { - assertThat(mp3.isDisposed()).isTrue(); - assertThat(mp3.getError()).hasMessage("test"); - }) - .verifyErrorMessage("test"); - } - - @Test - public void filterMonoProcessor() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - StepVerifier.create(mp.filter(s -> s % 2 == 0).subscribeWith(mp2), 0) - .then(() -> mp.onNext(2)) - .then(() -> assertThat(mp2.isError()).isFalse()) - .then(() -> assertThat(mp2.isDisposed()).isTrue()) - .then(() -> assertThat(mp2.peek()).isEqualTo(2)) - .then(() -> assertThat(mp2.isDisposed()).isTrue()) - .thenRequest(1) - .expectNext(2) - .verifyComplete(); - } - - @Test - public void filterMonoProcessorNot() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - StepVerifier.create(mp.filter(s -> s % 2 == 0).subscribeWith(mp2)) - .then(() -> mp.onNext(1)) - .then(() -> assertThat(mp2.isError()).isFalse()) - .then(() -> assertThat(mp2.isDisposed()).isTrue()) - .then(() -> assertThat(mp2.peek()).isNull()) - .then(() -> assertThat(mp2.isDisposed()).isTrue()) - .verifyComplete(); - } - - @Test - public void filterMonoProcessorError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - StepVerifier.create( - mp.filter( - s -> { - throw new RuntimeException("test"); - }) - .subscribeWith(mp2)) - .then(() -> mp.onNext(2)) - .then(() -> assertThat(mp2.isError()).isTrue()) - .then(() -> assertThat(mp2.isDisposed()).isTrue()) - .then(() -> assertThat(mp2.getError()).hasMessage("test")) - .then(() -> assertThat(mp2.isDisposed()).isTrue()) - .verifyErrorMessage("test"); - } - - @Test - public void doOnSuccessMonoProcessorError() { - UnicastMonoProcessor mp = UnicastMonoProcessor.create(); - UnicastMonoProcessor mp2 = UnicastMonoProcessor.create(); - AtomicReference ref = new AtomicReference<>(); - - StepVerifier.create( - mp.doOnSuccess( - s -> { - throw new RuntimeException("test"); - }) - .doOnError(ref::set) - .subscribeWith(mp2)) - .then(() -> mp.onNext(2)) - .then(() -> assertThat(mp2.isError()).isTrue()) - .then(() -> assertThat(ref.get()).hasMessage("test")) - .then(() -> assertThat(mp2.isDisposed()).isTrue()) - .then(() -> assertThat(mp2.getError()).hasMessage("test")) - .then(() -> assertThat(mp2.isDisposed()).isTrue()) - .verifyErrorMessage("test"); - } - - @Test - public void fluxCancelledByMonoProcessor() { - AtomicLong cancelCounter = new AtomicLong(); - Flux.range(1, 10) - .doOnCancel(cancelCounter::incrementAndGet) - .transformDeferred((s) -> s.subscribeWith(UnicastMonoProcessor.create())) - .subscribe(); - - assertThat(cancelCounter.get()).isEqualTo(1); - } - - @Test - public void cancelledByMonoProcessor() { - AtomicLong cancelCounter = new AtomicLong(); - UnicastMonoProcessor monoProcessor = - Mono.just("foo") - .doOnCancel(cancelCounter::incrementAndGet) - .subscribeWith(UnicastMonoProcessor.create()); - monoProcessor.subscribe(); - - assertThat(cancelCounter.get()).isEqualTo(1); - } - - @Test - public void scanProcessor() { - UnicastMonoProcessor test = UnicastMonoProcessor.create(); - Subscription subscription = Operators.emptySubscription(); - test.onSubscribe(subscription); - - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - - test.onComplete(); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isTrue(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - } - - @Test - public void scanProcessorCancelled() { - UnicastMonoProcessor test = UnicastMonoProcessor.create(); - Subscription subscription = Operators.emptySubscription(); - test.onSubscribe(subscription); - - assertThat(test.scan(Scannable.Attr.PREFETCH)).isEqualTo(Integer.MAX_VALUE); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isFalse(); - - test.cancel(); - assertThat(test.scan(Scannable.Attr.TERMINATED)).isFalse(); - assertThat(test.scan(Scannable.Attr.CANCELLED)).isTrue(); - } - - @Test - public void scanProcessorSubscription() { - UnicastMonoProcessor test = UnicastMonoProcessor.create(); - Subscription subscription = Operators.emptySubscription(); - test.onSubscribe(subscription); - - assertThat(test.scan(Scannable.Attr.ACTUAL)).isNull(); - assertThat(test.scan(Scannable.Attr.PARENT)).isSameAs(subscription); - } - - @Test - public void scanProcessorError() { - UnicastMonoProcessor test = UnicastMonoProcessor.create(); - Subscription subscription = Operators.emptySubscription(); - test.onSubscribe(subscription); - - test.onError(new IllegalStateException("boom")); - - assertThat(test.scan(Scannable.Attr.ERROR)).hasMessage("boom"); - } - - @Test - public void monoToProcessorConnects() { - TestPublisher tp = TestPublisher.create(); - UnicastMonoProcessor connectedProcessor = - tp.mono().subscribeWith(UnicastMonoProcessor.create()); - - assertThat(connectedProcessor.subscription).isNotNull(); - } - - @Test - public void monoToProcessorChain() { - StepVerifier.withVirtualTime( - () -> - Mono.just("foo") - .subscribeWith(UnicastMonoProcessor.create()) - .delayElement(Duration.ofMillis(500))) - .expectSubscription() - .expectNoEvent(Duration.ofMillis(500)) - .expectNext("foo") - .verifyComplete(); - } - - @Test - public void monoToProcessorChainColdToHot() { - AtomicInteger subscriptionCount = new AtomicInteger(); - Mono coldToHot = - Mono.just("foo") - .doOnSubscribe(sub -> subscriptionCount.incrementAndGet()) - .transformDeferred(s -> s.subscribeWith(UnicastMonoProcessor.create())) - .subscribeWith(UnicastMonoProcessor.create()) // this actually subscribes - .filter(s -> s.length() < 4); - - assertThat(subscriptionCount.get()).isEqualTo(1); - - coldToHot.block(); - assertThatThrownBy(coldToHot::block) - .hasMessage("UnicastMonoProcessor allows only a single Subscriber"); - assertThatThrownBy(coldToHot::block) - .hasMessage("UnicastMonoProcessor allows only a single Subscriber"); - - assertThat(subscriptionCount.get()).isEqualTo(1); - } - - @Test - public void monoProcessorBlockIsUnbounded() { - long start = System.nanoTime(); - - String result = - Mono.just("foo") - .delayElement(Duration.ofMillis(500)) - .subscribeWith(UnicastMonoProcessor.create()) - .block(); - - assertThat(result).isEqualTo("foo"); - assertThat(Duration.ofNanos(System.nanoTime() - start)) - .isGreaterThanOrEqualTo(Duration.ofMillis(500)); - } - - @Test - public void monoProcessorBlockNegativeIsImmediateTimeout() { - long start = System.nanoTime(); - - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy( - () -> - Mono.just("foo") - .delayElement(Duration.ofMillis(500)) - .subscribeWith(UnicastMonoProcessor.create()) - .block(Duration.ofSeconds(-1))) - .withMessage("Timeout on blocking read for -1000 MILLISECONDS"); - - assertThat(Duration.ofNanos(System.nanoTime() - start)).isLessThan(Duration.ofMillis(500)); - } - - @Test - public void monoProcessorBlockZeroIsImmediateTimeout() { - long start = System.nanoTime(); - - assertThatExceptionOfType(IllegalStateException.class) - .isThrownBy( - () -> - Mono.just("foo") - .delayElement(Duration.ofMillis(500)) - .subscribeWith(UnicastMonoProcessor.create()) - .block(Duration.ZERO)) - .withMessage("Timeout on blocking read for 0 MILLISECONDS"); - - assertThat(Duration.ofNanos(System.nanoTime() - start)).isLessThan(Duration.ofMillis(500)); - } - - @Test - public void disposeBeforeValueSendsCancellationException() { - UnicastMonoProcessor processor = UnicastMonoProcessor.create(); - AtomicReference e1 = new AtomicReference<>(); - AtomicReference e2 = new AtomicReference<>(); - AtomicReference e3 = new AtomicReference<>(); - AtomicReference late = new AtomicReference<>(); - - processor.subscribe(v -> Assertions.fail("expected first subscriber to error"), e1::set); - processor.subscribe(v -> Assertions.fail("expected second subscriber to error"), e2::set); - processor.subscribe(v -> Assertions.fail("expected third subscriber to error"), e3::set); - - processor.dispose(); - - assertThat(e1.get()).isInstanceOf(CancellationException.class); - assertThat(e2.get()).isInstanceOf(IllegalStateException.class); - assertThat(e3.get()).isInstanceOf(IllegalStateException.class); - - processor.subscribe(v -> Assertions.fail("expected late subscriber to error"), late::set); - assertThat(late.get()).isInstanceOf(IllegalStateException.class); - } -} From 388775c6bc36682e2fb45d68061e63bc8a3a42e1 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 30 Apr 2020 17:31:54 +0300 Subject: [PATCH 163/181] adds generated JMH files cleanup --- benchmarks/build.gradle | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/benchmarks/build.gradle b/benchmarks/build.gradle index fa1d6e04b..f07f7c6f5 100644 --- a/benchmarks/build.gradle +++ b/benchmarks/build.gradle @@ -40,6 +40,10 @@ task jmhBaseline(type: JmhExecTask, description: 'Executing JMH baseline benchma classpath = sourceSets.main.runtimeClasspath + configurations.baseline } +clean { + delete "${projectDir}/src/main/generated" +} + class JmhExecTask extends JavaExec { private String include; @@ -160,4 +164,4 @@ class JmhExecTask extends JavaExec { super.exec(); } -} \ No newline at end of file +} From d97fd8d5fc1d74f9fb72340b6f1023e1d780f1a8 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 30 Apr 2020 20:50:55 +0300 Subject: [PATCH 164/181] adds missing NPE catch in order to handle rare racing cases (#808) --- .../src/main/java/io/rsocket/core/RSocketRequester.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index f762bfe99..f9d4b2afe 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -478,7 +478,7 @@ public void accept(long n) { frame = RequestChannelFrameFlyweight.encodeReleasingPayload( allocator, streamId, false, n, initialPayload); - } catch (IllegalReferenceCountException e) { + } catch (IllegalReferenceCountException | NullPointerException e) { return; } From 12fd30140d23f161ad59c29c6385860af1d19a09 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 1 May 2020 10:51:20 +0300 Subject: [PATCH 165/181] provides supportive class to hide concurrency complexity (#807) --- .../io/rsocket/core/RSocketRequester.java | 417 ++++++++---------- .../java/io/rsocket/core/RequestOperator.java | 188 ++++++++ .../core/RSocketRequesterSubscribersTest.java | 30 ++ .../io/rsocket/core/RSocketRequesterTest.java | 155 ++++++- .../io/rsocket/core/SetupRejectionTest.java | 2 + 5 files changed, 566 insertions(+), 226 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index f9d4b2afe..fabea217b 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -52,10 +52,8 @@ import java.nio.channels.ClosedChannelException; import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; -import java.util.function.LongConsumer; import java.util.function.Supplier; import javax.annotation.Nullable; import org.reactivestreams.Processor; @@ -66,6 +64,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.MonoProcessor; +import reactor.core.publisher.Operators; import reactor.core.publisher.SignalType; import reactor.core.publisher.UnicastProcessor; import reactor.util.concurrent.Queues; @@ -208,7 +207,6 @@ private Mono handleFireAndForget(Payload payload) { } final AtomicBoolean once = new AtomicBoolean(); - final int streamId = streamIdSupplier.nextStreamId(receivers); return Mono.defer( () -> { @@ -217,15 +215,14 @@ private Mono handleFireAndForget(Payload payload) { new IllegalStateException("FireAndForgetMono allows only a single subscriber")); } - return Mono.empty() - .doOnSubscribe( - (__) -> { - ByteBuf requestFrame = - RequestFireAndForgetFrameFlyweight.encodeReleasingPayload( - allocator, streamId, payload); + final int streamId = streamIdSupplier.nextStreamId(receivers); + final ByteBuf requestFrame = + RequestFireAndForgetFrameFlyweight.encodeReleasingPayload( + allocator, streamId, payload); - sendProcessor.onNext(requestFrame); - }); + sendProcessor.onNext(requestFrame); + + return Mono.empty(); }); } @@ -241,13 +238,10 @@ private Mono handleRequestResponse(final Payload payload) { return Mono.error(new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); } - int streamId = streamIdSupplier.nextStreamId(receivers); final UnboundedProcessor sendProcessor = this.sendProcessor; final UnicastProcessor receiver = UnicastProcessor.create(Queues.one().get()); final AtomicBoolean once = new AtomicBoolean(); - receivers.put(streamId, receiver); - return Mono.defer( () -> { if (once.getAndSet(true)) { @@ -257,21 +251,39 @@ private Mono handleRequestResponse(final Payload payload) { return receiver .next() - .doOnSubscribe( - (__) -> { - ByteBuf requestFrame = - RequestResponseFrameFlyweight.encodeReleasingPayload( - allocator, streamId, payload); - - sendProcessor.onNext(requestFrame); - }) - .doFinally( - signalType -> { - if (signalType == SignalType.CANCEL) { - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - } - removeStreamReceiver(streamId); - }) + .transform( + Operators.lift( + (s, actual) -> + new RequestOperator(actual) { + + @Override + void hookOnFirstRequest(long n) { + int streamId = streamIdSupplier.nextStreamId(receivers); + this.streamId = streamId; + + ByteBuf requestResponseFrame = + RequestResponseFrameFlyweight.encodeReleasingPayload( + allocator, streamId, payload); + + receivers.put(streamId, receiver); + sendProcessor.onNext(requestResponseFrame); + } + + @Override + void hookOnCancel() { + if (receivers.remove(streamId, receiver)) { + sendProcessor.onNext( + CancelFrameFlyweight.encode(allocator, streamId)); + } else { + payload.release(); + } + } + + @Override + public void hookOnTerminal(SignalType signalType) { + receivers.remove(streamId, receiver); + } + })) .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); }); } @@ -288,15 +300,10 @@ private Flux handleRequestStream(final Payload payload) { return Flux.error(new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE)); } - int streamId = streamIdSupplier.nextStreamId(receivers); - final UnboundedProcessor sendProcessor = this.sendProcessor; final UnicastProcessor receiver = UnicastProcessor.create(); - final AtomicInteger wip = new AtomicInteger(0); final AtomicBoolean once = new AtomicBoolean(); - receivers.put(streamId, receiver); - return Flux.defer( () -> { if (once.getAndSet(true)) { @@ -305,62 +312,50 @@ private Flux handleRequestStream(final Payload payload) { } return receiver - .doOnRequest( - new LongConsumer() { - - boolean firstRequest = true; - - @Override - public void accept(long n) { - if (firstRequest) { - firstRequest = false; - if (wip.getAndIncrement() != 0) { - // no need to do anything. - // stream was canceled and fist payload has already been discarded - return; - } - int missed = 1; - boolean firstHasBeenSent = false; - for (; ; ) { - if (!firstHasBeenSent) { - sendProcessor.onNext( - RequestStreamFrameFlyweight.encodeReleasingPayload( - allocator, streamId, n, payload)); - firstHasBeenSent = true; - } else { - // if first frame was sent but we cycling again, it means that wip was - // incremented at doOnCancel - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - return; - } - - missed = wip.addAndGet(-missed); - if (missed == 0) { - return; - } - } - } else if (!receiver.isDisposed()) { - sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); - } - } - }) - .doFinally( - s -> { - if (s == SignalType.CANCEL) { - if (wip.getAndIncrement() != 0) { - return; - } - - // check if we need to release payload - // only applicable if the cancel appears earlier than actual request - if (payload.refCnt() > 0) { - payload.release(); - } else { - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - } - } - removeStreamReceiver(streamId); - }) + .transform( + Operators.lift( + (s, actual) -> + new RequestOperator(actual) { + + @Override + void hookOnFirstRequest(long n) { + int streamId = streamIdSupplier.nextStreamId(receivers); + this.streamId = streamId; + + ByteBuf requestStreamFrame = + RequestStreamFrameFlyweight.encodeReleasingPayload( + allocator, streamId, n, payload); + + receivers.put(streamId, receiver); + + sendProcessor.onNext(requestStreamFrame); + } + + @Override + void hookOnRemainingRequests(long n) { + if (receiver.isDisposed()) { + return; + } + + sendProcessor.onNext( + RequestNFrameFlyweight.encode(allocator, streamId, n)); + } + + @Override + void hookOnCancel() { + if (receivers.remove(streamId, receiver)) { + sendProcessor.onNext( + CancelFrameFlyweight.encode(allocator, streamId)); + } else { + payload.release(); + } + } + + @Override + void hookOnTerminal(SignalType signalType) { + receivers.remove(streamId); + } + })) .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); }); } @@ -394,141 +389,123 @@ private Flux handleChannel(Flux request) { private Flux handleChannel(Payload initialPayload, Flux inboundFlux) { final UnboundedProcessor sendProcessor = this.sendProcessor; - final int streamId = streamIdSupplier.nextStreamId(receivers); - final AtomicInteger wip = new AtomicInteger(0); final UnicastProcessor receiver = UnicastProcessor.create(); - final BaseSubscriber upstreamSubscriber = - new BaseSubscriber() { - boolean first = true; + return receiver.transform( + Operators.lift( + (s, actual) -> + new RequestOperator(actual) { - @Override - protected void hookOnSubscribe(Subscription subscription) { - // noops - } + final BaseSubscriber upstreamSubscriber = + new BaseSubscriber() { - @Override - protected void hookOnNext(Payload payload) { - if (first) { - // need to skip first since we have already sent it - // no need to release it since it was released earlier on the request establishment - // phase - first = false; - request(1); - return; - } - if (!PayloadValidationUtils.isValid(mtu, payload)) { - payload.release(); - cancel(); - final IllegalArgumentException t = - new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); - errorConsumer.accept(t); - // no need to send any errors. - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - receiver.onError(t); - return; - } - final ByteBuf frame = - PayloadFrameFlyweight.encodeNextReleasingPayload(allocator, streamId, payload); - - sendProcessor.onNext(frame); - } + boolean first = true; - @Override - protected void hookOnComplete() { - ByteBuf frame = PayloadFrameFlyweight.encodeComplete(allocator, streamId); - sendProcessor.onNext(frame); - } + @Override + protected void hookOnSubscribe(Subscription subscription) { + // noops + } - @Override - protected void hookOnError(Throwable t) { - ByteBuf frame = ErrorFrameFlyweight.encode(allocator, streamId, t); - sendProcessor.onNext(frame); - receiver.onError(t); - } + @Override + protected void hookOnNext(Payload payload) { + if (first) { + // need to skip first since we have already sent it + // no need to release it since it was released earlier on the request + // establishment + // phase + first = false; + request(1); + return; + } + if (!PayloadValidationUtils.isValid(mtu, payload)) { + payload.release(); + cancel(); + final IllegalArgumentException t = + new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); + errorConsumer.accept(t); + // no need to send any errors. + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + receiver.onError(t); + return; + } + final ByteBuf frame = + PayloadFrameFlyweight.encodeNextReleasingPayload( + allocator, streamId, payload); - @Override - protected void hookFinally(SignalType type) { - senders.remove(streamId, this); - } - }; - - return receiver - .doOnRequest( - new LongConsumer() { - - boolean firstRequest = true; - - @Override - public void accept(long n) { - if (firstRequest) { - firstRequest = false; - if (wip.getAndIncrement() != 0) { - // no need to do anything. - // stream was canceled and fist payload has already been discarded - return; + sendProcessor.onNext(frame); + } + + @Override + protected void hookOnComplete() { + ByteBuf frame = PayloadFrameFlyweight.encodeComplete(allocator, streamId); + sendProcessor.onNext(frame); + } + + @Override + protected void hookOnError(Throwable t) { + ByteBuf frame = ErrorFrameFlyweight.encode(allocator, streamId, t); + sendProcessor.onNext(frame); + receiver.onError(t); + } + + @Override + protected void hookFinally(SignalType type) { + senders.remove(streamId, this); + } + }; + + @Override + void hookOnFirstRequest(long n) { + final int streamId = streamIdSupplier.nextStreamId(receivers); + this.streamId = streamId; + + final ByteBuf frame = + RequestChannelFrameFlyweight.encodeReleasingPayload( + allocator, streamId, false, n, initialPayload); + + senders.put(streamId, upstreamSubscriber); + receivers.put(streamId, receiver); + + inboundFlux + .limitRate(Queues.SMALL_BUFFER_SIZE) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER) + .subscribe(upstreamSubscriber); + + sendProcessor.onNext(frame); } - int missed = 1; - boolean firstHasBeenSent = false; - for (; ; ) { - if (!firstHasBeenSent) { - ByteBuf frame; - try { - frame = - RequestChannelFrameFlyweight.encodeReleasingPayload( - allocator, streamId, false, n, initialPayload); - } catch (IllegalReferenceCountException | NullPointerException e) { - return; - } - - senders.put(streamId, upstreamSubscriber); - receivers.put(streamId, receiver); - - inboundFlux - .limitRate(Queues.SMALL_BUFFER_SIZE) - .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER) - .subscribe(upstreamSubscriber); - - sendProcessor.onNext(frame); - firstHasBeenSent = true; - } else { - // if first frame was sent but we cycling again, it means that wip was - // incremented at doOnCancel - senders.remove(streamId, upstreamSubscriber); - receivers.remove(streamId, receiver); - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + + @Override + void hookOnRemainingRequests(long n) { + if (receiver.isDisposed()) { return; } - missed = wip.addAndGet(-missed); - if (missed == 0) { - return; + sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); + } + + @Override + void hookOnCancel() { + senders.remove(streamId, upstreamSubscriber); + if (receivers.remove(streamId, receiver)) { + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); } } - } else { - sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); - } - } - }) - .doOnError( - t -> { - upstreamSubscriber.cancel(); - receivers.remove(streamId, receiver); - }) - .doOnComplete(() -> receivers.remove(streamId, receiver)) - .doOnCancel( - () -> { - upstreamSubscriber.cancel(); - if (wip.getAndIncrement() != 0) { - return; - } - // need to send frame only if RequestChannelFrame was sent - if (receivers.remove(streamId, receiver)) { - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - } - }); + @Override + void hookOnTerminal(SignalType signalType) { + if (signalType == SignalType.ON_ERROR) { + upstreamSubscriber.cancel(); + } + receivers.remove(streamId, receiver); + } + + @Override + public void cancel() { + upstreamSubscriber.cancel(); + super.cancel(); + } + })); } private Mono handleMetadataPush(Payload payload) { @@ -552,14 +529,12 @@ private Mono handleMetadataPush(Payload payload) { new IllegalStateException("MetadataPushMono allows only a single subscriber")); } - return Mono.empty() - .doOnSubscribe( - (__) -> { - ByteBuf metadataPushFrame = - MetadataPushFrameFlyweight.encodeReleasingPayload(allocator, payload); + ByteBuf metadataPushFrame = + MetadataPushFrameFlyweight.encodeReleasingPayload(allocator, payload); - sendProcessor.onNextPrioritized(metadataPushFrame); - }); + sendProcessor.onNextPrioritized(metadataPushFrame); + + return Mono.empty(); }); } @@ -757,14 +732,6 @@ private void terminate(Throwable e) { onClose.onError(e); } - private void removeStreamReceiver(int streamId) { - /*on termination receivers are explicitly cleared to avoid removing from map while iterating over one - of its views*/ - if (terminationError == null) { - receivers.remove(streamId); - } - } - private void handleSendProcessorError(Throwable t) { connection.dispose(); } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java b/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java new file mode 100644 index 000000000..05f8d6b3c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/core/RequestOperator.java @@ -0,0 +1,188 @@ +package io.rsocket.core; + +import io.rsocket.Payload; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import org.reactivestreams.Subscription; +import reactor.core.CoreSubscriber; +import reactor.core.Fuseable; +import reactor.core.publisher.Operators; +import reactor.core.publisher.SignalType; +import reactor.util.context.Context; + +/** + * This is a support class for handling of request input, intended for use with {@link + * Operators#lift}. It ensures serial execution of cancellation vs first request signals and also + * provides hooks for separate handling of first vs subsequent {@link Subscription#request} + * invocations. + */ +abstract class RequestOperator + implements CoreSubscriber, Fuseable.QueueSubscription { + + final CoreSubscriber actual; + + Subscription s; + Fuseable.QueueSubscription qs; + + int streamId; + boolean firstRequest = true; + + volatile int wip; + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(RequestOperator.class, "wip"); + + RequestOperator(CoreSubscriber actual) { + this.actual = actual; + } + + /** + * Optional hook executed exactly once on the first {@link Subscription#request) invocation + * and right after the {@link Subscription#request} was propagated to the upstream subscription. + * + *

    Note: this hook may not be invoked if cancellation happened before this invocation + */ + void hookOnFirstRequest(long n) {} + + /** + * Optional hook executed after the {@link Subscription#request} was propagated to the upstream + * subscription and excludes the first {@link Subscription#request} invocation. + */ + void hookOnRemainingRequests(long n) {} + + /** Optional hook executed after this {@link Subscription} cancelling. */ + void hookOnCancel() {} + + /** + * Optional hook executed after {@link org.reactivestreams.Subscriber} termination events + * (onError, onComplete). + * + * @param signalType the type of termination event that triggered the hook ({@link + * SignalType#ON_ERROR} or {@link SignalType#ON_COMPLETE}) + */ + void hookOnTerminal(SignalType signalType) {} + + @Override + public Context currentContext() { + return actual.currentContext(); + } + + @Override + public void request(long n) { + this.s.request(n); + if (!firstRequest) { + try { + this.hookOnRemainingRequests(n); + } catch (Throwable throwable) { + onError(throwable); + } + return; + } + this.firstRequest = false; + + if (WIP.getAndIncrement(this) != 0) { + return; + } + int missed = 1; + + boolean firstLoop = true; + for (; ; ) { + if (firstLoop) { + firstLoop = false; + try { + this.hookOnFirstRequest(n); + } catch (Throwable throwable) { + onError(throwable); + return; + } + } else { + try { + this.hookOnCancel(); + } catch (Throwable throwable) { + onError(throwable); + } + return; + } + + missed = WIP.addAndGet(this, -missed); + if (missed == 0) { + return; + } + } + } + + @Override + public void cancel() { + this.s.cancel(); + + if (WIP.getAndIncrement(this) != 0) { + return; + } + + hookOnCancel(); + } + + @Override + @SuppressWarnings("unchecked") + public void onSubscribe(Subscription s) { + if (Operators.validate(this.s, s)) { + this.s = s; + if (s instanceof Fuseable.QueueSubscription) { + this.qs = (Fuseable.QueueSubscription) s; + } + this.actual.onSubscribe(this); + } + } + + @Override + public void onNext(Payload t) { + this.actual.onNext(t); + } + + @Override + public void onError(Throwable t) { + this.actual.onError(t); + try { + this.hookOnTerminal(SignalType.ON_ERROR); + } catch (Throwable throwable) { + Operators.onErrorDropped(throwable, currentContext()); + } + } + + @Override + public void onComplete() { + this.actual.onComplete(); + try { + this.hookOnTerminal(SignalType.ON_COMPLETE); + } catch (Throwable throwable) { + Operators.onErrorDropped(throwable, currentContext()); + } + } + + @Override + public int requestFusion(int requestedMode) { + if (this.qs != null) { + return this.qs.requestFusion(requestedMode); + } else { + return Fuseable.NONE; + } + } + + @Override + public Payload poll() { + return this.qs.poll(); + } + + @Override + public int size() { + return this.qs.size(); + } + + @Override + public boolean isEmpty() { + return this.qs.isEmpty(); + } + + @Override + public void clear() { + this.qs.clear(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index fc87fc721..4a9d907fa 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -22,7 +22,9 @@ import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; +import io.rsocket.frame.PayloadFrameFlyweight; import io.rsocket.frame.decoder.PayloadDecoder; +import io.rsocket.internal.subscriber.AssertSubscriber; import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.DefaultPayload; @@ -40,6 +42,7 @@ import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; +import reactor.test.util.RaceTestUtils; class RSocketRequesterSubscribersTest { @@ -89,6 +92,33 @@ void singleSubscriber(Function> interaction) { Assertions.assertThat(requestFramesCount(connection.getSent())).isEqualTo(1); } + @ParameterizedTest + @MethodSource("allInteractions") + void singleSubscriberInCaseOfRacing(Function> interaction) { + for (int i = 1; i < 20000; i += 2) { + Flux response = Flux.from(interaction.apply(rSocketRequester)); + AssertSubscriber assertSubscriberA = AssertSubscriber.create(); + AssertSubscriber assertSubscriberB = AssertSubscriber.create(); + + RaceTestUtils.race( + () -> response.subscribe(assertSubscriberA), () -> response.subscribe(assertSubscriberB)); + + connection.addToReceivedBuffer(PayloadFrameFlyweight.encodeComplete(connection.alloc(), i)); + + assertSubscriberA.assertTerminated(); + assertSubscriberB.assertTerminated(); + + Assertions.assertThat(new AssertSubscriber[] {assertSubscriberA, assertSubscriberB}) + .anySatisfy(as -> as.assertError(IllegalStateException.class)); + + Assertions.assertThat(connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> REQUEST_TYPES.contains(FrameHeaderFlyweight.frameType(bb))); + connection.clearSendReceiveBuffers(); + } + } + @ParameterizedTest @MethodSource("allInteractions") void singleSubscriberInteractionsAreLazy(Function> interaction) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index b6067cdeb..d7cd8c24b 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -34,6 +34,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; import io.netty.util.ReferenceCountUtil; @@ -50,7 +51,9 @@ import io.rsocket.frame.FrameType; import io.rsocket.frame.PayloadFrameFlyweight; import io.rsocket.frame.RequestChannelFrameFlyweight; +import io.rsocket.frame.RequestFireAndForgetFrameFlyweight; import io.rsocket.frame.RequestNFrameFlyweight; +import io.rsocket.frame.RequestResponseFrameFlyweight; import io.rsocket.frame.RequestStreamFrameFlyweight; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.subscriber.AssertSubscriber; @@ -534,7 +537,8 @@ private static Stream racingCases() { RaceTestUtils.race(() -> as.request(1), as::cancel); // ensures proper frames order if (rule.connection.getSent().size() > 0) { - Assertions.assertThat(rule.connection.getSent()).hasSize(2); + // + // Assertions.assertThat(rule.connection.getSent()).hasSize(2); Assertions.assertThat(rule.connection.getSent()) .element(0) .matches( @@ -771,6 +775,155 @@ static Stream encodeDecodePayloadCases() { Arguments.of(REQUEST_CHANNEL, 5, 5)); } + @Test + public void ensuresThatNoOpsMustHappenUntilSubscriptionInCaseOfFnfCall() { + Payload payload1 = ByteBufPayload.create("abc1"); + Mono fnf1 = rule.socket.fireAndForget(payload1); + + Payload payload2 = ByteBufPayload.create("abc2"); + Mono fnf2 = rule.socket.fireAndForget(payload2); + + Assertions.assertThat(rule.connection.getSent()).isEmpty(); + + // checks that fnf2 should have id 1 even though it was generated later than fnf1 + AssertSubscriber voidAssertSubscriber2 = fnf2.subscribeWith(AssertSubscriber.create(0)); + voidAssertSubscriber2.assertTerminated().assertNoError(); + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> frameType(bb) == REQUEST_FNF) + .matches(bb -> FrameHeaderFlyweight.streamId(bb) == 1) + // ensures that this is fnf1 with abc2 data + .matches( + bb -> + ByteBufUtil.equals( + RequestFireAndForgetFrameFlyweight.data(bb), + Unpooled.wrappedBuffer("abc2".getBytes()))) + .matches(ReferenceCounted::release); + + rule.connection.clearSendReceiveBuffers(); + + // checks that fnf1 should have id 3 even though it was generated earlier + AssertSubscriber voidAssertSubscriber1 = fnf1.subscribeWith(AssertSubscriber.create(0)); + voidAssertSubscriber1.assertTerminated().assertNoError(); + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> frameType(bb) == REQUEST_FNF) + .matches(bb -> FrameHeaderFlyweight.streamId(bb) == 3) + // ensures that this is fnf1 with abc1 data + .matches( + bb -> + ByteBufUtil.equals( + RequestFireAndForgetFrameFlyweight.data(bb), + Unpooled.wrappedBuffer("abc1".getBytes()))) + .matches(ReferenceCounted::release); + } + + @ParameterizedTest + @MethodSource("requestNInteractions") + public void ensuresThatNoOpsMustHappenUntilFirstRequestN( + FrameType frameType, BiFunction> interaction) { + Payload payload1 = ByteBufPayload.create("abc1"); + Publisher interaction1 = interaction.apply(rule, payload1); + + Payload payload2 = ByteBufPayload.create("abc2"); + Publisher interaction2 = interaction.apply(rule, payload2); + + Assertions.assertThat(rule.connection.getSent()).isEmpty(); + + AssertSubscriber assertSubscriber1 = AssertSubscriber.create(0); + interaction1.subscribe(assertSubscriber1); + AssertSubscriber assertSubscriber2 = AssertSubscriber.create(0); + interaction2.subscribe(assertSubscriber2); + assertSubscriber1.assertNotTerminated().assertNoError(); + assertSubscriber2.assertNotTerminated().assertNoError(); + // even though we subscribed, nothing should happen until the first requestN + Assertions.assertThat(rule.connection.getSent()).isEmpty(); + + // first request on the second interaction to ensure that stream id issuing on the first request + assertSubscriber2.request(1); + + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> frameType(bb) == frameType) + .matches( + bb -> FrameHeaderFlyweight.streamId(bb) == 1, + "Expected to have stream ID {1} but got {" + + FrameHeaderFlyweight.streamId(rule.connection.getSent().iterator().next()) + + "}") + .matches( + bb -> { + switch (frameType) { + case REQUEST_RESPONSE: + return ByteBufUtil.equals( + RequestResponseFrameFlyweight.data(bb), + Unpooled.wrappedBuffer("abc2".getBytes())); + case REQUEST_STREAM: + return ByteBufUtil.equals( + RequestStreamFrameFlyweight.data(bb), + Unpooled.wrappedBuffer("abc2".getBytes())); + case REQUEST_CHANNEL: + return ByteBufUtil.equals( + RequestChannelFrameFlyweight.data(bb), + Unpooled.wrappedBuffer("abc2".getBytes())); + } + + return false; + }) + .matches(ReferenceCounted::release); + + rule.connection.clearSendReceiveBuffers(); + + assertSubscriber1.request(1); + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches(bb -> frameType(bb) == frameType) + .matches( + bb -> FrameHeaderFlyweight.streamId(bb) == 3, + "Expected to have stream ID {1} but got {" + + FrameHeaderFlyweight.streamId(rule.connection.getSent().iterator().next()) + + "}") + .matches( + bb -> { + switch (frameType) { + case REQUEST_RESPONSE: + return ByteBufUtil.equals( + RequestResponseFrameFlyweight.data(bb), + Unpooled.wrappedBuffer("abc1".getBytes())); + case REQUEST_STREAM: + return ByteBufUtil.equals( + RequestStreamFrameFlyweight.data(bb), + Unpooled.wrappedBuffer("abc1".getBytes())); + case REQUEST_CHANNEL: + return ByteBufUtil.equals( + RequestChannelFrameFlyweight.data(bb), + Unpooled.wrappedBuffer("abc1".getBytes())); + } + + return false; + }) + .matches(ReferenceCounted::release); + } + + private static Stream requestNInteractions() { + return Stream.of( + Arguments.of( + REQUEST_RESPONSE, + (BiFunction>) + (rule, payload) -> rule.socket.requestResponse(payload)), + Arguments.of( + REQUEST_STREAM, + (BiFunction>) + (rule, payload) -> rule.socket.requestStream(payload)), + Arguments.of( + REQUEST_CHANNEL, + (BiFunction>) + (rule, payload) -> rule.socket.requestChannel(Flux.just(payload)))); + } + public int sendRequestResponse(Publisher response) { Subscriber sub = TestSubscriber.create(); response.subscribe(sub); diff --git a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java index db72c7775..4d5cdc0d5 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -20,6 +20,7 @@ import java.time.Duration; import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.core.publisher.UnicastProcessor; @@ -47,6 +48,7 @@ void responderRejectSetup() { } @Test + @Disabled("FIXME: needs to be revised") void requesterStreamsTerminatedOnZeroErrorFrame() { LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); From b60020e7d5b451313c6a55522ec6029df2d3bb89 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 4 May 2020 11:00:23 +0300 Subject: [PATCH 166/181] provides `Payload` `refCnt` verification (#809) --- .../io/rsocket/core/RSocketRequester.java | 20 ++++++++ .../io/rsocket/core/RSocketResponder.java | 37 +++++++++++---- .../io/rsocket/core/RSocketRequesterTest.java | 25 ++++++++++ .../io/rsocket/core/RSocketResponderTest.java | 46 +++++++++++++++++++ 4 files changed, 118 insertions(+), 10 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index fabea217b..ced250620 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -195,6 +195,10 @@ public Mono onClose() { } private Mono handleFireAndForget(Payload payload) { + if (payload.refCnt() <= 0) { + return Mono.error(new IllegalReferenceCountException()); + } + Throwable err = checkAvailable(); if (err != null) { payload.release(); @@ -227,6 +231,10 @@ private Mono handleFireAndForget(Payload payload) { } private Mono handleRequestResponse(final Payload payload) { + if (payload.refCnt() <= 0) { + return Mono.error(new IllegalReferenceCountException()); + } + Throwable err = checkAvailable(); if (err != null) { payload.release(); @@ -289,6 +297,10 @@ public void hookOnTerminal(SignalType signalType) { } private Flux handleRequestStream(final Payload payload) { + if (payload.refCnt() <= 0) { + return Flux.error(new IllegalReferenceCountException()); + } + Throwable err = checkAvailable(); if (err != null) { payload.release(); @@ -371,6 +383,10 @@ private Flux handleChannel(Flux request) { (s, flux) -> { Payload payload = s.get(); if (payload != null) { + if (payload.refCnt() <= 0) { + return Mono.error(new IllegalReferenceCountException()); + } + if (!PayloadValidationUtils.isValid(mtu, payload)) { payload.release(); final IllegalArgumentException t = @@ -509,6 +525,10 @@ public void cancel() { } private Mono handleMetadataPush(Payload payload) { + if (payload.refCnt() <= 0) { + return Mono.error(new IllegalReferenceCountException()); + } + Throwable err = this.terminationError; if (err != null) { payload.release(); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index f5c4aecec..2f073ba8a 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -450,8 +450,32 @@ protected void hookOnSubscribe(Subscription s) { @Override protected void hookOnNext(Payload payload) { - if (!PayloadValidationUtils.isValid(mtu, payload)) { - payload.release(); + try { + if (!PayloadValidationUtils.isValid(mtu, payload)) { + payload.release(); + // specifically for requestChannel case so when Payload is invalid we will not be + // sending CancelFrame and ErrorFrame + // Note: CancelFrame is redundant and due to spec + // (https://github.com/rsocket/rsocket/blob/master/Protocol.md#request-channel) + // Upon receiving an ERROR[APPLICATION_ERROR|REJECTED|CANCELED|INVALID], the stream + // is + // terminated on both Requester and Responder. + // Upon sending an ERROR[APPLICATION_ERROR|REJECTED|CANCELED|INVALID], the stream is + // terminated on both the Requester and Responder. + if (requestChannel != null) { + channelProcessors.remove(streamId, requestChannel); + } + cancel(); + final IllegalArgumentException t = + new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); + handleError(streamId, t); + return; + } + + ByteBuf byteBuf = + PayloadFrameFlyweight.encodeNextReleasingPayload(allocator, streamId, payload); + sendProcessor.onNext(byteBuf); + } catch (Throwable e) { // specifically for requestChannel case so when Payload is invalid we will not be // sending CancelFrame and ErrorFrame // Note: CancelFrame is redundant and due to spec @@ -464,15 +488,8 @@ protected void hookOnNext(Payload payload) { channelProcessors.remove(streamId, requestChannel); } cancel(); - final IllegalArgumentException t = - new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); - handleError(streamId, t); - return; + handleError(streamId, e); } - - ByteBuf byteBuf = - PayloadFrameFlyweight.encodeNextReleasingPayload(allocator, streamId, payload); - sendProcessor.onNext(byteBuf); } @Override diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index d7cd8c24b..2117e195d 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -37,6 +37,7 @@ import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; import io.netty.util.CharsetUtil; +import io.netty.util.IllegalReferenceCountException; import io.netty.util.ReferenceCountUtil; import io.netty.util.ReferenceCounted; import io.rsocket.Payload; @@ -775,6 +776,30 @@ static Stream encodeDecodePayloadCases() { Arguments.of(REQUEST_CHANNEL, 5, 5)); } + @ParameterizedTest + @MethodSource("refCntCases") + public void ensureSendsErrorOnIllegalRefCntPayload( + BiFunction> sourceProducer) { + Payload invalidPayload = ByteBufPayload.create("test", "test"); + invalidPayload.release(); + + Publisher source = sourceProducer.apply(invalidPayload, rule.socket); + + StepVerifier.create(source, 0) + .expectError(IllegalReferenceCountException.class) + .verify(Duration.ofMillis(100)); + } + + private static Stream>> refCntCases() { + return Stream.of( + (p, r) -> r.fireAndForget(p), + (p, r) -> r.requestResponse(p), + (p, r) -> r.requestStream(p), + (p, r) -> r.requestChannel(Mono.just(p)), + (p, r) -> + r.requestChannel(Flux.just(EmptyPayload.INSTANCE, p).doOnSubscribe(s -> s.request(1)))); + } + @Test public void ensuresThatNoOpsMustHappenUntilSubscriptionInCaseOfFnfCall() { Payload payload1 = ByteBufPayload.create("abc1"); diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index 2dbf6715b..9ec2a2df1 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -18,6 +18,7 @@ import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; import static io.rsocket.frame.FrameHeaderFlyweight.frameType; +import static io.rsocket.frame.FrameType.ERROR; import static io.rsocket.frame.FrameType.REQUEST_CHANNEL; import static io.rsocket.frame.FrameType.REQUEST_FNF; import static io.rsocket.frame.FrameType.REQUEST_N; @@ -711,6 +712,51 @@ static Stream encodeDecodePayloadCases() { Arguments.of(REQUEST_CHANNEL, 5, 5)); } + @ParameterizedTest + @MethodSource("refCntCases") + public void ensureSendsErrorOnIllegalRefCntPayload(FrameType frameType) { + rule.setAcceptingSocket( + new RSocket() { + @Override + public Mono requestResponse(Payload payload) { + Payload invalidPayload = ByteBufPayload.create("test", "test"); + invalidPayload.release(); + return Mono.just(invalidPayload); + } + + @Override + public Flux requestStream(Payload payload) { + Payload invalidPayload = ByteBufPayload.create("test", "test"); + invalidPayload.release(); + return Flux.just(invalidPayload); + } + + @Override + public Flux requestChannel(Publisher payloads) { + Payload invalidPayload = ByteBufPayload.create("test", "test"); + invalidPayload.release(); + return Flux.just(invalidPayload); + } + }); + + rule.sendRequest(1, frameType); + + Assertions.assertThat(rule.connection.getSent()) + .hasSize(1) + .first() + .matches( + bb -> frameType(bb) == ERROR, + "Expect frame type to be {" + + ERROR + + "} but was {" + + frameType(rule.connection.getSent().iterator().next()) + + "}"); + } + + private static Stream refCntCases() { + return Stream.of(REQUEST_RESPONSE, REQUEST_STREAM, REQUEST_CHANNEL); + } + public static class ServerSocketRule extends AbstractSocketRule { private RSocket acceptingSocket; From 202e27f9d8c9e3b74c154f3d7cb6f3f1d45036d9 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 4 May 2020 13:58:54 +0300 Subject: [PATCH 167/181] provides ordered stream id issuing (#811) --- .../io/rsocket/core/RSocketConnector.java | 4 +- .../io/rsocket/core/RSocketRequester.java | 248 +++++++++--------- .../java/io/rsocket/core/RSocketServer.java | 4 +- .../test/java/io/rsocket/TestScheduler.java | 80 ++++++ .../java/io/rsocket/core/KeepAliveTest.java | 7 +- .../io/rsocket/core/RSocketLeaseTest.java | 4 +- .../core/RSocketRequesterSubscribersTest.java | 25 +- .../io/rsocket/core/RSocketRequesterTest.java | 48 +++- .../java/io/rsocket/core/RSocketTest.java | 4 +- .../io/rsocket/core/SetupRejectionTest.java | 6 +- 10 files changed, 292 insertions(+), 138 deletions(-) create mode 100644 rsocket-core/src/test/java/io/rsocket/TestScheduler.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index a7eed8c76..4e47109cf 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -42,6 +42,7 @@ import java.util.function.Supplier; import reactor.core.Disposable; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; import reactor.util.retry.Retry; public class RSocketConnector { @@ -293,7 +294,8 @@ public Mono connect(Supplier transportSupplier) { (int) keepAliveInterval.toMillis(), (int) keepAliveMaxLifeTime.toMillis(), keepAliveHandler, - requesterLeaseHandler); + requesterLeaseHandler, + Schedulers.single(Schedulers.parallel())); RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index ced250620..a2bd3d9fd 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -67,6 +67,7 @@ import reactor.core.publisher.Operators; import reactor.core.publisher.SignalType; import reactor.core.publisher.UnicastProcessor; +import reactor.core.scheduler.Scheduler; import reactor.util.concurrent.Queues; /** @@ -105,6 +106,7 @@ class RSocketRequester implements RSocket { private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; private volatile Throwable terminationError; private final MonoProcessor onClose; + private final Scheduler serialScheduler; RSocketRequester( DuplexConnection connection, @@ -115,7 +117,8 @@ class RSocketRequester implements RSocket { int keepAliveTickPeriod, int keepAliveAckTimeout, @Nullable KeepAliveHandler keepAliveHandler, - RequesterLeaseHandler leaseHandler) { + RequesterLeaseHandler leaseHandler, + Scheduler serialScheduler) { this.connection = connection; this.allocator = connection.alloc(); this.payloadDecoder = payloadDecoder; @@ -126,6 +129,7 @@ class RSocketRequester implements RSocket { this.senders = new SynchronizedIntObjectHashMap<>(); this.receivers = new SynchronizedIntObjectHashMap<>(); this.onClose = MonoProcessor.create(); + this.serialScheduler = serialScheduler; // DO NOT Change the order here. The Send processor must be subscribed to before receiving this.sendProcessor = new UnboundedProcessor<>(); @@ -212,22 +216,23 @@ private Mono handleFireAndForget(Payload payload) { final AtomicBoolean once = new AtomicBoolean(); - return Mono.defer( - () -> { - if (once.getAndSet(true)) { - return Mono.error( - new IllegalStateException("FireAndForgetMono allows only a single subscriber")); - } + return Mono.defer( + () -> { + if (once.getAndSet(true)) { + return Mono.error( + new IllegalStateException("FireAndForgetMono allows only a single subscriber")); + } - final int streamId = streamIdSupplier.nextStreamId(receivers); - final ByteBuf requestFrame = - RequestFireAndForgetFrameFlyweight.encodeReleasingPayload( - allocator, streamId, payload); + final int streamId = streamIdSupplier.nextStreamId(receivers); + final ByteBuf requestFrame = + RequestFireAndForgetFrameFlyweight.encodeReleasingPayload( + allocator, streamId, payload); - sendProcessor.onNext(requestFrame); + sendProcessor.onNext(requestFrame); - return Mono.empty(); - }); + return Mono.empty(); + }) + .subscribeOn(serialScheduler); } private Mono handleRequestResponse(final Payload payload) { @@ -292,6 +297,7 @@ public void hookOnTerminal(SignalType signalType) { receivers.remove(streamId, receiver); } })) + .subscribeOn(serialScheduler) .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); }); } @@ -368,6 +374,7 @@ void hookOnTerminal(SignalType signalType) { receivers.remove(streamId); } })) + .subscribeOn(serialScheduler, false) .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER); }); } @@ -408,120 +415,125 @@ private Flux handleChannel(Payload initialPayload, Flux receiver = UnicastProcessor.create(); - return receiver.transform( - Operators.lift( - (s, actual) -> - new RequestOperator(actual) { + return receiver + .transform( + Operators.lift( + (s, actual) -> + new RequestOperator(actual) { - final BaseSubscriber upstreamSubscriber = - new BaseSubscriber() { + final BaseSubscriber upstreamSubscriber = + new BaseSubscriber() { - boolean first = true; + boolean first = true; - @Override - protected void hookOnSubscribe(Subscription subscription) { - // noops - } + @Override + protected void hookOnSubscribe(Subscription subscription) { + // noops + } - @Override - protected void hookOnNext(Payload payload) { - if (first) { - // need to skip first since we have already sent it - // no need to release it since it was released earlier on the request - // establishment - // phase - first = false; - request(1); - return; - } - if (!PayloadValidationUtils.isValid(mtu, payload)) { - payload.release(); - cancel(); - final IllegalArgumentException t = - new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); - errorConsumer.accept(t); - // no need to send any errors. - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - receiver.onError(t); - return; - } - final ByteBuf frame = - PayloadFrameFlyweight.encodeNextReleasingPayload( - allocator, streamId, payload); - - sendProcessor.onNext(frame); - } + @Override + protected void hookOnNext(Payload payload) { + if (first) { + // need to skip first since we have already sent it + // no need to release it since it was released earlier on the + // request + // establishment + // phase + first = false; + request(1); + return; + } + if (!PayloadValidationUtils.isValid(mtu, payload)) { + payload.release(); + cancel(); + final IllegalArgumentException t = + new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); + errorConsumer.accept(t); + // no need to send any errors. + sendProcessor.onNext( + CancelFrameFlyweight.encode(allocator, streamId)); + receiver.onError(t); + return; + } + final ByteBuf frame = + PayloadFrameFlyweight.encodeNextReleasingPayload( + allocator, streamId, payload); + + sendProcessor.onNext(frame); + } + + @Override + protected void hookOnComplete() { + ByteBuf frame = + PayloadFrameFlyweight.encodeComplete(allocator, streamId); + sendProcessor.onNext(frame); + } + + @Override + protected void hookOnError(Throwable t) { + ByteBuf frame = ErrorFrameFlyweight.encode(allocator, streamId, t); + sendProcessor.onNext(frame); + receiver.onError(t); + } - @Override - protected void hookOnComplete() { - ByteBuf frame = PayloadFrameFlyweight.encodeComplete(allocator, streamId); - sendProcessor.onNext(frame); + @Override + protected void hookFinally(SignalType type) { + senders.remove(streamId, this); + } + }; + + @Override + void hookOnFirstRequest(long n) { + final int streamId = streamIdSupplier.nextStreamId(receivers); + this.streamId = streamId; + + final ByteBuf frame = + RequestChannelFrameFlyweight.encodeReleasingPayload( + allocator, streamId, false, n, initialPayload); + + senders.put(streamId, upstreamSubscriber); + receivers.put(streamId, receiver); + + inboundFlux + .limitRate(Queues.SMALL_BUFFER_SIZE) + .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER) + .subscribe(upstreamSubscriber); + + sendProcessor.onNext(frame); + } + + @Override + void hookOnRemainingRequests(long n) { + if (receiver.isDisposed()) { + return; } - @Override - protected void hookOnError(Throwable t) { - ByteBuf frame = ErrorFrameFlyweight.encode(allocator, streamId, t); - sendProcessor.onNext(frame); - receiver.onError(t); + sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); + } + + @Override + void hookOnCancel() { + senders.remove(streamId, upstreamSubscriber); + if (receivers.remove(streamId, receiver)) { + sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); } + } - @Override - protected void hookFinally(SignalType type) { - senders.remove(streamId, this); + @Override + void hookOnTerminal(SignalType signalType) { + if (signalType == SignalType.ON_ERROR) { + upstreamSubscriber.cancel(); } - }; - - @Override - void hookOnFirstRequest(long n) { - final int streamId = streamIdSupplier.nextStreamId(receivers); - this.streamId = streamId; - - final ByteBuf frame = - RequestChannelFrameFlyweight.encodeReleasingPayload( - allocator, streamId, false, n, initialPayload); - - senders.put(streamId, upstreamSubscriber); - receivers.put(streamId, receiver); - - inboundFlux - .limitRate(Queues.SMALL_BUFFER_SIZE) - .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER) - .subscribe(upstreamSubscriber); - - sendProcessor.onNext(frame); - } - - @Override - void hookOnRemainingRequests(long n) { - if (receiver.isDisposed()) { - return; - } - - sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); - } - - @Override - void hookOnCancel() { - senders.remove(streamId, upstreamSubscriber); - if (receivers.remove(streamId, receiver)) { - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); - } - } - - @Override - void hookOnTerminal(SignalType signalType) { - if (signalType == SignalType.ON_ERROR) { - upstreamSubscriber.cancel(); - } - receivers.remove(streamId, receiver); - } - - @Override - public void cancel() { - upstreamSubscriber.cancel(); - super.cancel(); - } - })); + receivers.remove(streamId, receiver); + } + + @Override + public void cancel() { + upstreamSubscriber.cancel(); + super.cancel(); + } + })) + .subscribeOn(serialScheduler, false); } private Mono handleMetadataPush(Payload payload) { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index 19f0c5008..d5d8cee0f 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -39,6 +39,7 @@ import java.util.function.Consumer; import java.util.function.Supplier; import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; public final class RSocketServer { private static final String SERVER_TAG = "server"; @@ -222,7 +223,8 @@ private Mono acceptSetup( setupPayload.keepAliveInterval(), setupPayload.keepAliveMaxLifetime(), keepAliveHandler, - requesterLeaseHandler); + requesterLeaseHandler, + Schedulers.single(Schedulers.parallel())); RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); diff --git a/rsocket-core/src/test/java/io/rsocket/TestScheduler.java b/rsocket-core/src/test/java/io/rsocket/TestScheduler.java new file mode 100644 index 000000000..7bc98d45d --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/TestScheduler.java @@ -0,0 +1,80 @@ +package io.rsocket; + +import java.util.Queue; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import reactor.core.Disposable; +import reactor.core.Disposables; +import reactor.core.Exceptions; +import reactor.core.scheduler.Scheduler; +import reactor.util.concurrent.Queues; + +/** + * This is an implementation of scheduler which allows task execution on the caller thread or + * scheduling it for thread which are currently working (with "work stealing" behaviour) + */ +public final class TestScheduler implements Scheduler { + + public static final Scheduler INSTANCE = new TestScheduler(); + + volatile int wip; + static final AtomicIntegerFieldUpdater WIP = + AtomicIntegerFieldUpdater.newUpdater(TestScheduler.class, "wip"); + + final Worker sharedWorker = new TestWorker(this); + final Queue tasks = Queues.unboundedMultiproducer().get(); + + private TestScheduler() {} + + @Override + public Disposable schedule(Runnable task) { + tasks.offer(task); + if (WIP.getAndIncrement(this) != 0) { + return Disposables.never(); + } + + int missed = 1; + + for (; ; ) { + for (; ; ) { + Runnable runnable = tasks.poll(); + + if (runnable == null) { + break; + } + + try { + runnable.run(); + } catch (Throwable t) { + Exceptions.throwIfFatal(t); + } + } + + missed = WIP.addAndGet(this, -missed); + if (missed == 0) { + return Disposables.never(); + } + } + } + + @Override + public Worker createWorker() { + return sharedWorker; + } + + static class TestWorker implements Worker { + + final TestScheduler parent; + + TestWorker(TestScheduler parent) { + this.parent = parent; + } + + @Override + public Disposable schedule(Runnable task) { + return parent.schedule(task); + } + + @Override + public void dispose() {} + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java index e8f3f4190..7e465db08 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java @@ -23,6 +23,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.rsocket.RSocket; +import io.rsocket.TestScheduler; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.frame.FrameHeaderFlyweight; @@ -67,7 +68,8 @@ static RSocketState requester(int tickPeriod, int timeout) { tickPeriod, timeout, new DefaultKeepAliveHandler(connection), - RequesterLeaseHandler.None); + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); return new RSocketState(rSocket, errors, allocator, connection); } @@ -94,7 +96,8 @@ static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { tickPeriod, timeout, new ResumableKeepAliveHandler(resumableConnection), - RequesterLeaseHandler.None); + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); return new ResumableRSocketState(rSocket, errors, connection, resumableConnection, allocator); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java index 51f5afc24..04d5fe174 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -28,6 +28,7 @@ import io.netty.buffer.Unpooled; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.TestScheduler; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.Exceptions; import io.rsocket.frame.FrameHeaderFlyweight; @@ -98,7 +99,8 @@ void setUp() { 0, 0, null, - requesterLeaseHandler); + requesterLeaseHandler, + TestScheduler.INSTANCE); RSocket mockRSocketHandler = mock(RSocket.class); when(mockRSocketHandler.metadataPush(any())).thenReturn(Mono.empty()); diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index 4a9d907fa..3e7479af3 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -19,6 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.RSocket; +import io.rsocket.TestScheduler; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.FrameType; @@ -28,7 +29,6 @@ import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.DefaultPayload; -import java.time.Duration; import java.util.Arrays; import java.util.Collection; import java.util.HashSet; @@ -41,7 +41,6 @@ import org.junit.jupiter.params.provider.MethodSource; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; -import reactor.test.StepVerifier; import reactor.test.util.RaceTestUtils; class RSocketRequesterSubscribersTest { @@ -73,21 +72,25 @@ void setUp() { 0, 0, null, - RequesterLeaseHandler.None); + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); } @ParameterizedTest @MethodSource("allInteractions") void singleSubscriber(Function> interaction) { Flux response = Flux.from(interaction.apply(rSocketRequester)); - StepVerifier.withVirtualTime(() -> response.take(Duration.ofMillis(10))) - .thenAwait(Duration.ofMillis(10)) - .expectComplete() - .verify(Duration.ofSeconds(5)); - StepVerifier.withVirtualTime(() -> response.take(Duration.ofMillis(10))) - .thenAwait(Duration.ofMillis(10)) - .expectError(IllegalStateException.class) - .verify(Duration.ofSeconds(5)); + + AssertSubscriber assertSubscriberA = AssertSubscriber.create(); + AssertSubscriber assertSubscriberB = AssertSubscriber.create(); + + response.subscribe(assertSubscriberA); + response.subscribe(assertSubscriberB); + + connection.addToReceivedBuffer(PayloadFrameFlyweight.encodeComplete(connection.alloc(), 1)); + + assertSubscriberA.assertTerminated(); + assertSubscriberB.assertTerminated(); Assertions.assertThat(requestFramesCount(connection.getSent())).isEqualTo(1); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index 2117e195d..56e6c9342 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -42,6 +42,7 @@ import io.netty.util.ReferenceCounted; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.TestScheduler; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.CustomRSocketException; import io.rsocket.exceptions.RejectedSetupException; @@ -949,6 +950,50 @@ private static Stream requestNInteractions() { (rule, payload) -> rule.socket.requestChannel(Flux.just(payload)))); } + @ParameterizedTest + @MethodSource("streamIdRacingCases") + public void ensuresCorrectOrderOfStreamIdIssuingInCaseOfRacing( + BiFunction> interaction1, + BiFunction> interaction2) { + for (int i = 1; i < 10000; i += 4) { + Payload payload = DefaultPayload.create("test"); + Publisher publisher1 = interaction1.apply(rule, payload); + Publisher publisher2 = interaction2.apply(rule, payload); + RaceTestUtils.race( + () -> publisher1.subscribe(AssertSubscriber.create()), + () -> publisher2.subscribe(AssertSubscriber.create())); + + Assertions.assertThat(rule.connection.getSent()) + .extracting(FrameHeaderFlyweight::streamId) + .containsExactly(i, i + 2); + rule.connection.getSent().clear(); + } + } + + public static Stream streamIdRacingCases() { + return Stream.of( + Arguments.of( + (BiFunction>) + (r, p) -> r.socket.fireAndForget(p), + (BiFunction>) + (r, p) -> r.socket.requestResponse(p)), + Arguments.of( + (BiFunction>) + (r, p) -> r.socket.requestResponse(p), + (BiFunction>) + (r, p) -> r.socket.requestStream(p)), + Arguments.of( + (BiFunction>) + (r, p) -> r.socket.requestStream(p), + (BiFunction>) + (r, p) -> r.socket.requestChannel(Flux.just(p))), + Arguments.of( + (BiFunction>) + (r, p) -> r.socket.requestChannel(Flux.just(p)), + (BiFunction>) + (r, p) -> r.socket.fireAndForget(p))); + } + public int sendRequestResponse(Publisher response) { Subscriber sub = TestSubscriber.create(); response.subscribe(sub); @@ -973,7 +1018,8 @@ protected RSocketRequester newRSocket() { 0, 0, null, - RequesterLeaseHandler.None); + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); } public int getStreamIdForRequestType(FrameType expectedFrameType) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index 02c3dfca8..48ce150d6 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -25,6 +25,7 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.Payload; import io.rsocket.RSocket; +import io.rsocket.TestScheduler; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.CustomRSocketException; @@ -492,7 +493,8 @@ public Flux requestChannel(Publisher payloads) { 0, 0, null, - RequesterLeaseHandler.None); + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); } public void setRequestAcceptor(RSocket requestAcceptor) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java index 4d5cdc0d5..388bfffeb 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -64,7 +64,8 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { 0, 0, null, - RequesterLeaseHandler.None); + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); String errorMsg = "error"; @@ -101,7 +102,8 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { 0, 0, null, - RequesterLeaseHandler.None); + RequesterLeaseHandler.None, + TestScheduler.INSTANCE); conn.addToReceivedBuffer( ErrorFrameFlyweight.encode( From 4fa73124212c02ffa31fb4d0589cd289b1260446 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 4 May 2020 13:31:26 +0100 Subject: [PATCH 168/181] Add Javadoc to RSocketConnector and RSocketServer (#813) --- .../io/rsocket/core/RSocketConnector.java | 371 ++++++++++++++---- .../java/io/rsocket/core/RSocketServer.java | 228 +++++++++-- .../src/main/java/io/rsocket/core/Resume.java | 105 ++++- .../FragmentationDuplexConnection.java | 2 +- .../core/RSocketServerFragmentationTest.java | 6 +- 5 files changed, 598 insertions(+), 114 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 4e47109cf..3d9345c4b 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -22,6 +22,7 @@ import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; +import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.frame.SetupFrameFlyweight; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.ClientServerInputMultiplexer; @@ -40,14 +41,36 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; +import javax.annotation.Nullable; import reactor.core.Disposable; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import reactor.util.retry.Retry; +/** + * The main class to use to establish a connection to an RSocket server. + * + *

    To connect over TCP using default settings: + * + *

    {@code
    + * import io.rsocket.transport.netty.client.TcpClientTransport;
    + *
    + * Mono rocketMono =
    + *         RSocketConnector.connectWith(TcpClientTransport.create("localhost", 7000));
    + * }
    + * + *

    To customize connection settings before connecting: + * + *

    {@code
    + * Mono rocketMono =
    + *         RSocketConnector.create()
    + *                 .metadataMimeType("message/x.rsocket.composite-metadata.v0")
    + *                 .dataMimeType("application/cbor")
    + *                 .connect(TcpClientTransport.create("localhost", 7000));
    + * }
    + */ public class RSocketConnector { private static final String CLIENT_TAG = "client"; - private static final int MIN_MTU_SIZE = 64; private static final BiConsumer INVALIDATE_FUNCTION = (r, i) -> r.onClose().subscribe(null, __ -> i.invalidate(), i::invalidate); @@ -55,13 +78,12 @@ public class RSocketConnector { private Payload setupPayload = EmptyPayload.INSTANCE; private String metadataMimeType = "application/binary"; private String dataMimeType = "application/binary"; - - private SocketAcceptor acceptor = SocketAcceptor.with(new RSocket() {}); - private InitializingInterceptorRegistry interceptors = new InitializingInterceptorRegistry(); - private Duration keepAliveInterval = Duration.ofSeconds(20); private Duration keepAliveMaxLifeTime = Duration.ofSeconds(90); + @Nullable private SocketAcceptor acceptor; + private InitializingInterceptorRegistry interceptors = new InitializingInterceptorRegistry(); + private Retry retrySpec; private Resume resume; private Supplier> leasesSupplier; @@ -73,53 +95,109 @@ public class RSocketConnector { private RSocketConnector() {} + /** + * Static factory method to create an {@code RSocketConnector} instance and customize default + * settings before connecting. To connect only, use {@link #connectWith(ClientTransport)}. + */ public static RSocketConnector create() { return new RSocketConnector(); } + /** + * Static factory method to connect with default settings, effectively a shortcut for: + * + *
    +   * RSocketConnector.create().connectWith(transport);
    +   * 
    + * + * @param transport the transport of choice to connect with + * @return a {@code Mono} with the connected RSocket + */ public static Mono connectWith(ClientTransport transport) { return RSocketConnector.create().connect(() -> transport); } + /** + * Provide a {@code Payload} with data and/or metadata for the initial {@code SETUP} frame. Data + * and metadata should be formatted according to the MIME types specified via {@link + * #dataMimeType(String)} and {@link #metadataMimeType(String)}. + * + * @param payload the payload containing data and/or metadata for the {@code SETUP} frame + * @return the same instance for method chaining + * @see SETUP + * Frame + */ public RSocketConnector setupPayload(Payload payload) { - this.setupPayload = payload; + this.setupPayload = Objects.requireNonNull(payload); return this; } + /** + * Set the MIME type to use for formatting payload data on the established connection. This is set + * in the initial {@code SETUP} frame sent to the server. + * + *

    By default this is set to {@code "application/binary"}. + * + * @param dataMimeType the MIME type to be used for payload data + * @return the same instance for method chaining + * @see SETUP + * Frame + */ public RSocketConnector dataMimeType(String dataMimeType) { - this.dataMimeType = dataMimeType; + this.dataMimeType = Objects.requireNonNull(dataMimeType); return this; } + /** + * Set the MIME type to use for formatting payload metadata on the established connection. This is + * set in the initial {@code SETUP} frame sent to the server. + * + *

    For metadata encoding, consider using one of the following encoders: + * + *

      + *
    • {@link io.rsocket.metadata.CompositeMetadataFlyweight Composite Metadata} + *
    • {@link io.rsocket.metadata.TaggingMetadataFlyweight Routing} + *
    • {@link io.rsocket.metadata.security.AuthMetadataFlyweight Authentication} + *
    + * + *

    For more on the above metadata formats, see the corresponding protocol extensions + * + *

    By default this is set to {@code "application/binary"}. + * + * @param metadataMimeType the MIME type to be used for payload metadata + * @return the same instance for method chaining + * @see SETUP + * Frame + */ public RSocketConnector metadataMimeType(String metadataMimeType) { - this.metadataMimeType = metadataMimeType; - return this; - } - - public RSocketConnector interceptors(Consumer consumer) { - consumer.accept(this.interceptors); - return this; - } - - public RSocketConnector acceptor(SocketAcceptor acceptor) { - this.acceptor = acceptor; + this.metadataMimeType = Objects.requireNonNull(metadataMimeType); return this; } /** - * Set the time {@code interval} between KEEPALIVE frames sent by this client, and the {@code - * maxLifeTime} that this client will allow between KEEPALIVE frames from the server before - * assuming it is dead. + * Set the "Time Between {@code KEEPALIVE} Frames" which is how frequently {@code KEEPALIVE} + * frames should be emitted, and the "Max Lifetime" which is how long to allow between {@code + * KEEPALIVE} frames from the remote end before concluding that connectivity is lost. Both + * settings are specified in the initial {@code SETUP} frame sent to the server. The spec mentions + * the following: * - *

    Note that reasonable values for the time interval may vary significantly. For - * server-to-server connections the spec suggests 500ms, while for for mobile-to-server - * connections it suggests 30+ seconds. In addition {@code maxLifeTime} should allow plenty of - * room for multiple missed ticks from the server. + *

      + *
    • For server-to-server connections, a reasonable time interval between client {@code + * KEEPALIVE} frames is 500ms. + *
    • For mobile-to-server connections, the time interval between client {@code KEEPALIVE} + * frames is often > 30,000ms. + *
    * - *

    By default {@code interval} is set to 20 seconds and {@code maxLifeTime} to 90 seconds. + *

    By default these are set to 20 seconds and 90 seconds respectively. * - * @param interval the time between KEEPALIVE frames sent, must be greater than 0. - * @param maxLifeTime the max time between KEEPALIVE frames received, must be greater than 0. + * @param interval how frequently to emit KEEPALIVE frames + * @param maxLifeTime how long to allow between {@code KEEPALIVE} frames from the remote end + * before assuming that connectivity is lost; the value should be generous and allow for + * multiple missed {@code KEEPALIVE} frames. + * @return the same instance for method chaining + * @see SETUP + * Frame */ public RSocketConnector keepAlive(Duration interval, Duration maxLifeTime) { if (!interval.negated().isNegative()) { @@ -134,97 +212,227 @@ public RSocketConnector keepAlive(Duration interval, Duration maxLifeTime) { } /** - * Enables a reconnectable, shared instance of {@code Mono} so every subscriber will - * observe the same RSocket instance up on connection establishment.
    - * For example: + * Configure interception at one of the following levels: + * + *

      + *
    • Transport level + *
    • At the level of accepting new connections + *
    • Performing requests + *
    • Responding to requests + *
    + * + * @param configurer a configurer to customize interception with. + * @return the same instance for method chaining + */ + public RSocketConnector interceptors(Consumer configurer) { + configurer.accept(this.interceptors); + return this; + } + + /** + * Configure a client-side {@link SocketAcceptor} for responding to requests from the server. + * + *

    A full-form example with access to the {@code SETUP} frame and the "sending" RSocket (the + * same as the one returned from {@link #connect(ClientTransport)}): * *

    {@code
    -   * Mono sharedRSocketMono =
    -   *   RSocketConnector.create()
    -   *           .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
    -   *           .connect(transport);
    +   * Mono rsocketMono =
    +   *     RSocketConnector.create()
    +   *             .acceptor((setup, sendingRSocket) -> Mono.just(new RSocket() {...}))
    +   *             .connect(transport);
    +   * }
    * - * RSocket r1 = sharedRSocketMono.block(); - * RSocket r2 = sharedRSocketMono.block(); + *

    A shortcut example with just the handling RSocket: * - * assert r1 == r2; + *

    {@code
    +   * Mono rsocketMono =
    +   *     RSocketConnector.create()
    +   *             .acceptor(SocketAcceptor.with(new RSocket() {...})))
    +   *             .connect(transport);
    +   * }
    * + *

    A shortcut example handling only request-response: + * + *

    {@code
    +   * Mono rsocketMono =
    +   *     RSocketConnector.create()
    +   *             .acceptor(SocketAcceptor.forRequestResponse(payload -> ...))
    +   *             .connect(transport);
        * }
    * - * Apart of the shared behavior, if the connection is lost, the same {@code Mono} - * instance will transparently re-establish the connection for subsequent subscribers.
    - * For example: + *

    By default, {@code new RSocket(){}} is used which rejects all requests from the server with + * {@link UnsupportedOperationException}. + * + * @param acceptor the acceptor to use for responding to server requests + * @return the same instance for method chaining + */ + public RSocketConnector acceptor(SocketAcceptor acceptor) { + this.acceptor = acceptor; + return this; + } + + /** + * When this is enabled, the connect methods of this class return a special {@code Mono} + * that maintains a single, shared {@code RSocket} for all subscribers: * *

    {@code
    -   * Mono sharedRSocketMono =
    +   * Mono rsocketMono =
        *   RSocketConnector.create()
        *           .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
        *           .connect(transport);
        *
    -   *  RSocket r1 = sharedRSocketMono.block();
    -   *  RSocket r2 = sharedRSocketMono.block();
    +   *  RSocket r1 = rsocketMono.block();
    +   *  RSocket r2 = rsocketMono.block();
        *
        *  assert r1 == r2;
    +   * }
    * - * r1.dispose() + *

    The {@code RSocket} remains cached until the connection is lost and after that, new attempts + * to subscribe or re-subscribe trigger a reconnect and result in a new shared {@code RSocket}: * - * assert r2.isDisposed() + *

    {@code
    +   * Mono rsocketMono =
    +   *   RSocketConnector.create()
    +   *           .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
    +   *           .connect(transport);
    +   *
    +   *  RSocket r1 = rsocketMono.block();
    +   *  RSocket r2 = rsocketMono.block();
        *
    -   *  RSocket r3 = sharedRSocketMono.block();
    -   *  RSocket r4 = sharedRSocketMono.block();
    +   *  r1.dispose();
        *
    +   *  RSocket r3 = rsocketMono.block();
    +   *  RSocket r4 = rsocketMono.block();
    +   *
    +   *  assert r1 == r2;
    +   *  assert r3 == r4;
        *  assert r1 != r3;
    -   *  assert r4 == r3;
        *
        * }
    * - * Note, having reconnect() enabled does not eliminate the need to accompany each - * individual request with the corresponding retry logic.
    - * For example: + *

    Downstream subscribers for individual requests still need their own retry logic to determine + * if or when failed requests should be retried which in turn triggers the shared reconnect: * *

    {@code
    -   * Mono sharedRSocketMono =
    +   * Mono rocketMono =
        *   RSocketConnector.create()
        *           .reconnect(Retry.fixedDelay(3, Duration.ofSeconds(1)))
        *           .connect(transport);
        *
    -   *  sharedRSocket.flatMap(rSocket -> rSocket.requestResponse(...))
    -   *               .retryWhen(ownRetry)
    -   *               .subscribe()
    -   *
    +   *  rsocketMono.flatMap(rsocket -> rsocket.requestResponse(...))
    +   *           .retryWhen(Retry.fixedDelay(1, Duration.ofSeconds(5)))
    +   *           .subscribe()
        * }
    * - * @param retrySpec a retry factory applied for {@link Mono#retryWhen(Retry)} - * @return a shared instance of {@code Mono}. + *

    Note: this feature is mutually exclusive with {@link #resume(Resume)}. If + * both are enabled, "resume" takes precedence. Consider using "reconnect" when the server does + * not have "resume" enabled or supported, or when you don't need to incur the overhead of saving + * in-flight frames to be potentially replayed after a reconnect. + * + *

    By default this is not enabled in which case a new connection is obtained per subscriber. + * + * @param retry a retry spec that declares the rules for reconnecting + * @return the same instance for method chaining */ - public RSocketConnector reconnect(Retry retrySpec) { - this.retrySpec = Objects.requireNonNull(retrySpec); + public RSocketConnector reconnect(Retry retry) { + this.retrySpec = Objects.requireNonNull(retry); return this; } + /** + * Enables the Resume capability of the RSocket protocol where if the client gets disconnected, + * the connection is re-acquired and any interrupted streams are resumed automatically. For this + * to work the server must also support and have the Resume capability enabled. + * + *

    See {@link Resume} for settings to customize the Resume capability. + * + *

    Note: this feature is mutually exclusive with {@link #reconnect(Retry)}. If + * both are enabled, "resume" takes precedence. Consider using "reconnect" when the server does + * not have "resume" enabled or supported, or when you don't need to incur the overhead of saving + * in-flight frames to be potentially replayed after a reconnect. + * + *

    By default this is not enabled. + * + * @param resume configuration for the Resume capability + * @return the same instance for method chaining + * @see Resuming + * Operation + */ public RSocketConnector resume(Resume resume) { this.resume = resume; return this; } + /** + * Enables the Lease feature of the RSocket protocol where the number of requests that can be + * performed from either side are rationed via {@code LEASE} frames from the responder side. + * + *

    Example usage: + * + *

    {@code
    +   * Mono rocketMono =
    +   *         RSocketConnector.create().lease(Leases::new).connect(transport);
    +   * }
    + * + *

    By default this is not enabled. + * + * @param supplier supplier for a {@link Leases} + * @return the same instance for method chaining Lease + * Semantics + */ public RSocketConnector lease(Supplier> supplier) { this.leasesSupplier = supplier; return this; } + /** + * When this is set, frames larger than the given maximum transmission unit (mtu) size value are + * broken down into fragments to fit that size. + * + *

    By default this is not set in which case payloads are sent whole up to the maximum frame + * size of 16,777,215 bytes. + * + * @param mtu the threshold size for fragmentation, must be no less than 64 + * @return the same instance for method chaining + * @see Fragmentation + * and Reassembly + */ public RSocketConnector fragment(int mtu) { - if (mtu > 0 && mtu < MIN_MTU_SIZE || mtu < 0) { + if (mtu > 0 && mtu < FragmentationDuplexConnection.MIN_MTU_SIZE || mtu < 0) { String msg = - String.format("smallest allowed mtu size is %d bytes, provided: %d", MIN_MTU_SIZE, mtu); + String.format( + "The smallest allowed mtu size is %d bytes, provided: %d", + FragmentationDuplexConnection.MIN_MTU_SIZE, mtu); throw new IllegalArgumentException(msg); } this.mtu = mtu; return this; } - public RSocketConnector payloadDecoder(PayloadDecoder payloadDecoder) { - Objects.requireNonNull(payloadDecoder); - this.payloadDecoder = payloadDecoder; + /** + * Configure the {@code PayloadDecoder} used to create {@link Payload}'s from incoming raw frame + * buffers. The following decoders are available: + * + *

      + *
    • {@link PayloadDecoder#DEFAULT} -- the data and metadata are independent copies of the + * underlying frame {@link ByteBuf} + *
    • {@link PayloadDecoder#ZERO_COPY} -- the data and metadata are retained slices of the + * underlying {@link ByteBuf}. That's more efficient but requires careful tracking and + * {@link Payload#release() release} of the payload when no longer needed. + *
    + * + *

    By default this is set to {@link PayloadDecoder#DEFAULT} in which case data and metadata are + * copied and do not need to be tracked and released. + * + * @param decoder the decoder to use + * @return the same instance for method chaining + */ + public RSocketConnector payloadDecoder(PayloadDecoder decoder) { + Objects.requireNonNull(decoder); + this.payloadDecoder = decoder; return this; } @@ -239,10 +447,38 @@ public RSocketConnector errorConsumer(Consumer errorConsumer) { return this; } + /** + * The final step to connect with the transport to use as input and the resulting {@code + * Mono} as output. Each subscriber to the returned {@code Mono} starts a new connection + * if neither {@link #reconnect(Retry) reconnect} nor {@link #resume(Resume)} are enabled. + * + *

    The following transports are available (via additional RSocket Java modules): + * + *

      + *
    • {@link io.rsocket.transport.netty.client.TcpClientTransport TcpClientTransport} via + * {@code rsocket-transport-netty}. + *
    • {@link io.rsocket.transport.netty.client.WebsocketClientTransport + * WebsocketClientTransport} via {@code rsocket-transport-netty}. + *
    • {@link io.rsocket.transport.local.LocalClientTransport LocalClientTransport} via {@code + * rsocket-transport-local} + *
    + * + * @param transport the transport of choice to connect with + * @return a {@code Mono} with the connected RSocket + */ public Mono connect(ClientTransport transport) { return connect(() -> transport); } + /** + * Variant of {@link #connect(ClientTransport)} with a {@link Supplier} for the {@code + * ClientTransport}. + * + *

    // TODO: when to use? + * + * @param transportSupplier supplier for the transport to connect with + * @return a {@code Mono} with the connected RSocket + */ public Mono connect(Supplier transportSupplier) { Mono connectionMono = Mono.fromSupplier(transportSupplier).flatMap(t -> t.connect(mtu)); @@ -310,6 +546,9 @@ public Mono connect(Supplier transportSupplier) { dataMimeType, setupPayload); + SocketAcceptor acceptor = + this.acceptor != null ? this.acceptor : SocketAcceptor.with(new RSocket() {}); + ConnectionSetupPayload setup = new DefaultConnectionSetupPayload(setupFrame); return interceptors diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index d5d8cee0f..036900be1 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -20,10 +20,12 @@ import io.rsocket.Closeable; import io.rsocket.ConnectionSetupPayload; import io.rsocket.DuplexConnection; +import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; import io.rsocket.exceptions.InvalidSetupException; import io.rsocket.exceptions.RejectedSetupException; +import io.rsocket.fragmentation.FragmentationDuplexConnection; import io.rsocket.frame.FrameHeaderFlyweight; import io.rsocket.frame.SetupFrameFlyweight; import io.rsocket.frame.decoder.PayloadDecoder; @@ -41,64 +43,207 @@ import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; +/** + * The main class for starting an RSocket server. + * + *

    For example: + * + *

    {@code
    + * CloseableChannel closeable =
    + *         RSocketServer.create(SocketAcceptor.with(new RSocket() {...}))
    + *                 .bind(TcpServerTransport.create("localhost", 7000))
    + *                 .block();
    + * }
    + */ public final class RSocketServer { private static final String SERVER_TAG = "server"; - private static final int MIN_MTU_SIZE = 64; private SocketAcceptor acceptor = SocketAcceptor.with(new RSocket() {}); private InitializingInterceptorRegistry interceptors = new InitializingInterceptorRegistry(); - private int mtu = 0; private Resume resume; private Supplier> leasesSupplier = null; - private Consumer errorConsumer = ex -> {}; + private int mtu = 0; private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; + private Consumer errorConsumer = ex -> {}; + private RSocketServer() {} + /** Static factory method to create an {@code RSocketServer}. */ public static RSocketServer create() { return new RSocketServer(); } + /** + * Static factory method to create an {@code RSocketServer} instance with the given {@code + * SocketAcceptor}. Effectively a shortcut for: + * + *
    +   * RSocketServer.create().acceptor(...);
    +   * 
    + * + * @param acceptor the acceptor to handle connections with + * @return the same instance for method chaining + * @see #acceptor(SocketAcceptor) + */ public static RSocketServer create(SocketAcceptor acceptor) { return RSocketServer.create().acceptor(acceptor); } + /** + * Set the acceptor to handle incoming connections and handle requests. + * + *

    An example with access to the {@code SETUP} frame and sending RSocket for performing + * requests back to the client if needed: + * + *

    {@code
    +   * RSocketServer.create((setup, sendingRSocket) -> Mono.just(new RSocket() {...}))
    +   *         .bind(TcpServerTransport.create("localhost", 7000))
    +   *         .subscribe();
    +   * }
    + * + *

    A shortcut to provide the handling RSocket only: + * + *

    {@code
    +   * RSocketServer.create(SocketAcceptor.with(new RSocket() {...}))
    +   *         .bind(TcpServerTransport.create("localhost", 7000))
    +   *         .subscribe();
    +   * }
    + * + *

    A shortcut to handle request-response interactions only: + * + *

    {@code
    +   * RSocketServer.create(SocketAcceptor.forRequestResponse(payload -> ...))
    +   *         .bind(TcpServerTransport.create("localhost", 7000))
    +   *         .subscribe();
    +   * }
    + * + *

    By default, {@code new RSocket(){}} is used for handling which rejects requests from the + * client with {@link UnsupportedOperationException}. + * + * @param acceptor the acceptor to handle incoming connections and requests with + * @return the same instance for method chaining + */ public RSocketServer acceptor(SocketAcceptor acceptor) { Objects.requireNonNull(acceptor); this.acceptor = acceptor; return this; } - public RSocketServer interceptors(Consumer consumer) { - consumer.accept(this.interceptors); - return this; - } - - public RSocketServer fragment(int mtu) { - if (mtu > 0 && mtu < MIN_MTU_SIZE || mtu < 0) { - String msg = - String.format("smallest allowed mtu size is %d bytes, provided: %d", MIN_MTU_SIZE, mtu); - throw new IllegalArgumentException(msg); - } - this.mtu = mtu; + /** + * Configure interception at one of the following levels: + * + *

      + *
    • Transport level + *
    • At the level of accepting new connections + *
    • Performing requests + *
    • Responding to requests + *
    + * + * @param configurer a configurer to customize interception with. + * @return the same instance for method chaining + */ + public RSocketServer interceptors(Consumer configurer) { + configurer.accept(this.interceptors); return this; } + /** + * Enables the Resume capability of the RSocket protocol where if the client gets disconnected, + * the connection is re-acquired and any interrupted streams are transparently resumed. For this + * to work clients must also support and request to enable this when connecting. + * + *

    Use the {@link Resume} argument to customize the Resume session duration, storage, retry + * logic, and others. + * + *

    By default this is not enabled. + * + * @param resume configuration for the Resume capability + * @return the same instance for method chaining + * @see Resuming + * Operation + */ public RSocketServer resume(Resume resume) { this.resume = resume; return this; } + /** + * Enables the Lease feature of the RSocket protocol where the number of requests that can be + * performed from either side are rationed via {@code LEASE} frames from the responder side. For + * this to work clients must also support and request to enable this when connecting. + * + *

    Example usage: + * + *

    {@code
    +   * RSocketServer.create(SocketAcceptor.with(new RSocket() {...}))
    +   *         .lease(Leases::new)
    +   *         .bind(TcpServerTransport.create("localhost", 7000))
    +   *         .subscribe();
    +   * }
    + * + *

    By default this is not enabled. + * + * @param supplier supplier for a {@link Leases} + * @return the same instance for method chaining + * @return the same instance for method chaining Lease + * Semantics + */ public RSocketServer lease(Supplier> supplier) { this.leasesSupplier = supplier; return this; } - public RSocketServer payloadDecoder(PayloadDecoder payloadDecoder) { - Objects.requireNonNull(payloadDecoder); - this.payloadDecoder = payloadDecoder; + /** + * When this is set, frames larger than the given maximum transmission unit (mtu) size value are + * fragmented. + * + *

    By default this is not set in which case payloads are sent whole up to the maximum frame + * size of 16,777,215 bytes. + * + * @param mtu the threshold size for fragmentation, must be no less than 64 + * @return the same instance for method chaining + * @see Fragmentation + * and Reassembly + */ + public RSocketServer fragment(int mtu) { + if (mtu > 0 && mtu < FragmentationDuplexConnection.MIN_MTU_SIZE || mtu < 0) { + String msg = + String.format( + "The smallest allowed mtu size is %d bytes, provided: %d", + FragmentationDuplexConnection.MIN_MTU_SIZE, mtu); + throw new IllegalArgumentException(msg); + } + this.mtu = mtu; + return this; + } + + /** + * Configure the {@code PayloadDecoder} used to create {@link Payload}'s from incoming raw frame + * buffers. The following decoders are available: + * + *

      + *
    • {@link PayloadDecoder#DEFAULT} -- the data and metadata are independent copies of the + * underlying frame {@link ByteBuf} + *
    • {@link PayloadDecoder#ZERO_COPY} -- the data and metadata are retained slices of the + * underlying {@link ByteBuf}. That's more efficient but requires careful tracking and + * {@link Payload#release() release} of the payload when no longer needed. + *
    + * + *

    By default this is set to {@link PayloadDecoder#DEFAULT} in which case data and metadata are + * copied and do not need to be tracked and released. + * + * @param decoder the decoder to use + * @return the same instance for method chaining + */ + public RSocketServer payloadDecoder(PayloadDecoder decoder) { + Objects.requireNonNull(decoder); + this.payloadDecoder = decoder; return this; } @@ -112,17 +257,25 @@ public RSocketServer errorConsumer(Consumer errorConsumer) { return this; } - public ServerTransport.ConnectionAcceptor asConnectionAcceptor() { - return new ServerTransport.ConnectionAcceptor() { - private final ServerSetup serverSetup = serverSetup(); - - @Override - public Mono apply(DuplexConnection connection) { - return acceptor(serverSetup, connection); - } - }; - } - + /** + * Start the server on the given transport. + * + *

    The following transports are available from additional RSocket Java modules: + * + *

      + *
    • {@link io.rsocket.transport.netty.client.TcpServerTransport TcpServerTransport} via + * {@code rsocket-transport-netty}. + *
    • {@link io.rsocket.transport.netty.client.WebsocketServerTransport + * WebsocketServerTransport} via {@code rsocket-transport-netty}. + *
    • {@link io.rsocket.transport.local.LocalServerTransport LocalServerTransport} via {@code + * rsocket-transport-local} + *
    + * + * @param transport the transport of choice to connect with + * @param the type of {@code Closeable} for the given transport + * @return a {@code Mono} with a {@code Closeable} that can be used to obtain information about + * the server, stop it, or be notified of when it is stopped. + */ public Mono bind(ServerTransport transport) { return Mono.defer( new Supplier>() { @@ -137,6 +290,23 @@ public Mono get() { }); } + /** + * An alternative to {@link #bind(ServerTransport)} that is useful for installing RSocket on a + * server that is started independently. + * + * @see io.rsocket.examples.transport.ws.WebSocketHeadersSample + */ + public ServerTransport.ConnectionAcceptor asConnectionAcceptor() { + return new ServerTransport.ConnectionAcceptor() { + private final ServerSetup serverSetup = serverSetup(); + + @Override + public Mono apply(DuplexConnection connection) { + return acceptor(serverSetup, connection); + } + }; + } + private Mono acceptor(ServerSetup serverSetup, DuplexConnection connection) { ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(connection, interceptors, false); diff --git a/rsocket-core/src/main/java/io/rsocket/core/Resume.java b/rsocket-core/src/main/java/io/rsocket/core/Resume.java index aedcc9e5e..04221154f 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/Resume.java +++ b/rsocket-core/src/main/java/io/rsocket/core/Resume.java @@ -20,20 +20,29 @@ import io.rsocket.resume.InMemoryResumableFramesStore; import io.rsocket.resume.ResumableFramesStore; import java.time.Duration; +import java.util.Objects; import java.util.function.Function; import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.util.retry.Retry; +/** + * Simple holder of configuration settings for the RSocket Resume capability. This can be used to + * configure an {@link RSocketConnector} or an {@link RSocketServer} except for {@link + * #retry(Retry)} and {@link #token(Supplier)} which apply only to the client side. + */ public class Resume { private static final Logger logger = LoggerFactory.getLogger(Resume.class); private Duration sessionDuration = Duration.ofMinutes(2); - private Duration streamTimeout = Duration.ofSeconds(10); + + /* Storage */ private boolean cleanupStoreOnKeepAlive; private Function storeFactory; + private Duration streamTimeout = Duration.ofSeconds(10); + /* Client only */ private Supplier tokenSupplier = ResumeFrameFlyweight::generateResumeToken; private Retry retry = Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(1)) @@ -43,43 +52,105 @@ public class Resume { public Resume() {} + /** + * The maximum time for a client to keep trying to reconnect. During this time client and server + * continue to store unsent frames to keep the session warm and ready to resume. + * + *

    By default this is set to 2 minutes. + * + * @param sessionDuration the max duration for a session + * @return the same instance for method chaining + */ public Resume sessionDuration(Duration sessionDuration) { - this.sessionDuration = sessionDuration; - return this; - } - - public Resume streamTimeout(Duration streamTimeout) { - this.streamTimeout = streamTimeout; + this.sessionDuration = Objects.requireNonNull(sessionDuration); return this; } + /** + * When this property is enabled, hints from {@code KEEPALIVE} frames about how much data has been + * received by the other side, is used to proactively clean frames from the {@link + * #storeFactory(Function) store}. + * + *

    By default this is set to {@code false} in which case information from {@code KEEPALIVE} is + * ignored and old frames from the store are removed only when the store runs out of space. + * + * @return the same instance for method chaining + */ public Resume cleanupStoreOnKeepAlive() { this.cleanupStoreOnKeepAlive = true; return this; } + /** + * Configure a factory to create the storage for buffering (or persisting) a window of frames that + * may need to be sent again to resume after a dropped connection. + * + *

    By default {@link InMemoryResumableFramesStore} is used with its cache size set to 100,000 + * bytes. When the cache fills up, the oldest frames are gradually removed to create space for new + * ones. + * + * @param storeFactory the factory to use to create the store + * @return the same instance for method chaining + */ public Resume storeFactory( Function storeFactory) { this.storeFactory = storeFactory; return this; } - public Resume token(Supplier supplier) { - this.tokenSupplier = supplier; + /** + * A {@link reactor.core.publisher.Flux#timeout(Duration) timeout} value to apply to the resumed + * session stream obtained from the {@link #storeFactory(Function) store} after a reconnect. The + * resume stream must not take longer than the specified time to emit each frame. + * + *

    By default this is set to 10 seconds. + * + * @param streamTimeout the timeout value for resuming a session stream + * @return the same instance for method chaining + */ + public Resume streamTimeout(Duration streamTimeout) { + this.streamTimeout = Objects.requireNonNull(streamTimeout); return this; } + /** + * Configure the logic for reconnecting. This setting is for use with {@link + * RSocketConnector#resume(Resume)} on the client side only. + * + *

    By default this is set to: + * + *

    {@code
    +   * Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(1))
    +   *     .maxBackoff(Duration.ofSeconds(16))
    +   *     .jitter(1.0)
    +   * }
    + * + * @param retry the {@code Retry} spec to use when attempting to reconnect + * @return the same instance for method chaining + */ public Resume retry(Retry retry) { this.retry = retry; return this; } - Duration getSessionDuration() { - return sessionDuration; + /** + * Customize the generation of the resume identification token used to resume. This setting is for + * use with {@link RSocketConnector#resume(Resume)} on the client side only. + * + *

    By default this is {@code ResumeFrameFlyweight::generateResumeToken}. + * + * @param supplier a custom generator for a resume identification token + * @return the same instance for method chaining + */ + public Resume token(Supplier supplier) { + this.tokenSupplier = supplier; + return this; } - Duration getStreamTimeout() { - return streamTimeout; + // Package private accessors + + Duration getSessionDuration() { + return sessionDuration; } boolean isCleanupStoreOnKeepAlive() { @@ -92,11 +163,15 @@ boolean isCleanupStoreOnKeepAlive() { : token -> new InMemoryResumableFramesStore(tag, 100_000); } - Supplier getTokenSupplier() { - return tokenSupplier; + Duration getStreamTimeout() { + return streamTimeout; } Retry getRetry() { return retry; } + + Supplier getTokenSupplier() { + return tokenSupplier; + } } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java index 316643e10..24b360755 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java @@ -41,7 +41,7 @@ */ public final class FragmentationDuplexConnection extends ReassemblyDuplexConnection implements DuplexConnection { - private static final int MIN_MTU_SIZE = 64; + public static final int MIN_MTU_SIZE = 64; private static final Logger logger = LoggerFactory.getLogger(FragmentationDuplexConnection.class); private final DuplexConnection delegate; private final int mtu; diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java index 9d105a8c9..073ebfd06 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketServerFragmentationTest.java @@ -11,7 +11,7 @@ public class RSocketServerFragmentationTest { public void serverErrorsWithEnabledFragmentationOnInsufficientMtu() { Assertions.assertThatIllegalArgumentException() .isThrownBy(() -> RSocketServer.create().fragment(2)) - .withMessage("smallest allowed mtu size is 64 bytes, provided: 2"); + .withMessage("The smallest allowed mtu size is 64 bytes, provided: 2"); } @Test @@ -28,12 +28,12 @@ public void serverSucceedsWithDisabledFragmentation() { public void clientErrorsWithEnabledFragmentationOnInsufficientMtu() { Assertions.assertThatIllegalArgumentException() .isThrownBy(() -> RSocketConnector.create().fragment(2)) - .withMessage("smallest allowed mtu size is 64 bytes, provided: 2"); + .withMessage("The smallest allowed mtu size is 64 bytes, provided: 2"); } @Test public void clientSucceedsWithEnabledFragmentationOnSufficientMtu() { - RSocketConnector.create().fragment(100).connect(TestClientTransport::new).block(); + RSocketConnector.create().fragment(100).connect(new TestClientTransport()).block(); } @Test From c9a4f064ca1fe8fb06b67f7ac09ba2777bbb5634 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 4 May 2020 15:32:20 +0300 Subject: [PATCH 169/181] relaxed stream id supplement (#814) --- .../java/io/rsocket/core/StreamIdSupplier.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java b/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java index 70734b8c0..7f4d7b7b3 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java +++ b/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -16,14 +16,11 @@ package io.rsocket.core; import io.netty.util.collection.IntObjectMap; -import java.util.concurrent.atomic.AtomicLongFieldUpdater; final class StreamIdSupplier { private static final int MASK = 0x7FFFFFFF; - private static final AtomicLongFieldUpdater STREAM_ID = - AtomicLongFieldUpdater.newUpdater(StreamIdSupplier.class, "streamId"); - private volatile long streamId; + private long streamId; // Visible for testing StreamIdSupplier(int streamId) { @@ -38,10 +35,18 @@ static StreamIdSupplier serverSupplier() { return new StreamIdSupplier(0); } + /** + * This methods provides new stream id and ensures there is no intersections with already running + * streams. This methods is not thread-safe. + * + * @param streamIds currently running streams store + * @return next stream id + */ int nextStreamId(IntObjectMap streamIds) { int streamId; do { - streamId = (int) STREAM_ID.addAndGet(this, 2) & MASK; + this.streamId += 2; + streamId = (int) (this.streamId & MASK); } while (streamId == 0 || streamIds.containsKey(streamId)); return streamId; } From be382d7dba2464ae7421bc7b720f70aab71d518c Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Wed, 6 May 2020 18:26:15 +0100 Subject: [PATCH 170/181] Add missing package-infos and Javadoc in plugins package (#817) --- .../io/rsocket/core/RSocketConnector.java | 4 +- .../java/io/rsocket/core/RSocketServer.java | 4 +- .../java/io/rsocket/core/package-info.java | 8 ++- .../io/rsocket/exceptions/package-info.java | 4 +- .../java/io/rsocket/frame/package-info.java | 21 +++++++ .../io/rsocket/keepalive/package-info.java | 19 ++++++ .../java/io/rsocket/lease/package-info.java | 9 ++- .../io/rsocket/metadata/package-info.java | 22 +++++++ .../main/java/io/rsocket/package-info.java | 11 +++- .../plugins/DuplexConnectionInterceptor.java | 8 ++- .../InitializingInterceptorRegistry.java | 4 ++ .../rsocket/plugins/InterceptorRegistry.java | 63 +++++++++++++++---- .../rsocket/plugins/RSocketInterceptor.java | 9 ++- .../plugins/SocketAcceptorInterceptor.java | 6 +- .../java/io/rsocket/plugins/package-info.java | 18 ++++++ .../java/io/rsocket/resume/package-info.java | 7 +++ .../io/rsocket/transport/package-info.java | 3 +- .../java/io/rsocket/util/package-info.java | 3 +- 18 files changed, 191 insertions(+), 32 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/package-info.java create mode 100644 rsocket-core/src/main/java/io/rsocket/keepalive/package-info.java create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/package-info.java create mode 100644 rsocket-core/src/main/java/io/rsocket/plugins/package-info.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 3d9345c4b..45d16a665 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -378,8 +378,8 @@ public RSocketConnector resume(Resume resume) { *

    By default this is not enabled. * * @param supplier supplier for a {@link Leases} - * @return the same instance for method chaining Lease + * @return the same instance for method chaining + * @see Lease * Semantics */ public RSocketConnector lease(Supplier> supplier) { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index 036900be1..5960e33d4 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -189,8 +189,8 @@ public RSocketServer resume(Resume resume) { * * @param supplier supplier for a {@link Leases} * @return the same instance for method chaining - * @return the same instance for method chaining Lease + * @return the same instance for method chaining + * @see Lease * Semantics */ public RSocketServer lease(Supplier> supplier) { diff --git a/rsocket-core/src/main/java/io/rsocket/core/package-info.java b/rsocket-core/src/main/java/io/rsocket/core/package-info.java index a70bb3b16..29db3f205 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/core/package-info.java @@ -15,8 +15,12 @@ */ /** - * Contains core RSocket protocol, client and server implementation classes, including factories to - * create and configure them. + * Contains {@link io.rsocket.core.RSocketConnector RSocketConnector} and {@link + * io.rsocket.core.RSocketServer RSocketServer}, the main classes for connecting to or starting an + * RSocket server. + * + *

    This package also contains a package private classes that implement support for the main + * RSocket interactions. */ @NonNullApi package io.rsocket.core; diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/package-info.java b/rsocket-core/src/main/java/io/rsocket/exceptions/package-info.java index babf8194e..969aedded 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -15,7 +15,7 @@ */ /** - * The hierarchy of exceptions that can be returned by the API + * A hierarchy of exceptions that represent RSocket protocol error codes. * * @see Error * Codes diff --git a/rsocket-core/src/main/java/io/rsocket/frame/package-info.java b/rsocket-core/src/main/java/io/rsocket/frame/package-info.java new file mode 100644 index 000000000..1d02ebca0 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/package-info.java @@ -0,0 +1,21 @@ +/* + * 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. + */ + +/** + * Support for encoding and decoding of RSocket frames to and from {@link io.rsocket.Payload + * Payload}. + */ +package io.rsocket.frame; diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/package-info.java b/rsocket-core/src/main/java/io/rsocket/keepalive/package-info.java new file mode 100644 index 000000000..ce8a2f3fb --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/package-info.java @@ -0,0 +1,19 @@ +/* + * 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. + */ + +/** Support classes for sending and keeping track of KEEPALIVE frames from the remote. */ +@javax.annotation.ParametersAreNonnullByDefault +package io.rsocket.keepalive; diff --git a/rsocket-core/src/main/java/io/rsocket/lease/package-info.java b/rsocket-core/src/main/java/io/rsocket/lease/package-info.java index 6700c10d9..ce1956628 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -14,5 +14,12 @@ * limitations under the License. */ +/** + * Contains support classes for the Lease feature of the RSocket protocol. + * + * @see Resuming + * Operation + */ @javax.annotation.ParametersAreNonnullByDefault package io.rsocket.lease; diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/package-info.java b/rsocket-core/src/main/java/io/rsocket/metadata/package-info.java new file mode 100644 index 000000000..b1bc45ff0 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/package-info.java @@ -0,0 +1,22 @@ +/* + * 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. + */ + +/** + * Contains implementations of RSocket protocol extensions related + * to the use of metadata. + */ +package io.rsocket.metadata; diff --git a/rsocket-core/src/main/java/io/rsocket/package-info.java b/rsocket-core/src/main/java/io/rsocket/package-info.java index 243c1ab52..878a56301 100644 --- a/rsocket-core/src/main/java/io/rsocket/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -14,4 +14,13 @@ * limitations under the License. */ +/** + * Contains key contracts of the RSocket programming model including {@link io.rsocket.RSocket + * RSocket} for performing or handling RSocket interactions, {@link io.rsocket.SocketAcceptor + * SocketAcceptor} for declaring responders, {@link io.rsocket.Payload Payload} for access to the + * content of a payload, and others. + * + *

    To connect to or start a server see {@link io.rsocket.core.RSocketConnector RSocketConnector} + * and {@link io.rsocket.core.RSocketServer RSocketServer} in {@link io.rsocket.core}. + */ package io.rsocket; diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java index 056ded0cd..6b2a7a71b 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/DuplexConnectionInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -19,9 +19,13 @@ import io.rsocket.DuplexConnection; import java.util.function.BiFunction; -/** */ +/** + * Contract to decorate a {@link DuplexConnection} and intercept the sending and receiving of + * RSocket frames at the transport level. + */ public @FunctionalInterface interface DuplexConnectionInterceptor extends BiFunction { + enum Type { SETUP, CLIENT, diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java index cf911b954..fc032847c 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/InitializingInterceptorRegistry.java @@ -19,6 +19,10 @@ import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; +/** + * Extends {@link InterceptorRegistry} with methods for building a chain of registered interceptors. + * This is not intended for direct use by applications. + */ public class InitializingInterceptorRegistry extends InterceptorRegistry { public DuplexConnection initConnection( diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java b/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java index f9ee151a8..427fa15ae 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/InterceptorRegistry.java @@ -19,54 +19,87 @@ import java.util.List; import java.util.function.Consumer; +/** + * Provides support for registering interceptors at the following levels: + * + *

      + *
    • {@link #forConnection(DuplexConnectionInterceptor)} -- transport level + *
    • {@link #forSocketAcceptor(SocketAcceptorInterceptor)} -- for accepting new connections + *
    • {@link #forRequester(RSocketInterceptor)} -- for performing of requests + *
    • {@link #forResponder(RSocketInterceptor)} -- for responding to requests + *
    + */ public class InterceptorRegistry { - private List connectionInterceptors = new ArrayList<>(); private List requesterInteceptors = new ArrayList<>(); private List responderInterceptors = new ArrayList<>(); private List socketAcceptorInterceptors = new ArrayList<>(); + private List connectionInterceptors = new ArrayList<>(); - public InterceptorRegistry forConnection(DuplexConnectionInterceptor interceptor) { - connectionInterceptors.add(interceptor); - return this; - } - - public InterceptorRegistry forConnection(Consumer> consumer) { - consumer.accept(connectionInterceptors); - return this; - } - + /** + * Add an {@link RSocketInterceptor} that will decorate the RSocket used for performing requests. + */ public InterceptorRegistry forRequester(RSocketInterceptor interceptor) { requesterInteceptors.add(interceptor); return this; } + /** + * Variant of {@link #forRequester(RSocketInterceptor)} with access to the list of existing + * registrations. + */ public InterceptorRegistry forRequester(Consumer> consumer) { consumer.accept(requesterInteceptors); return this; } + /** + * Add an {@link RSocketInterceptor} that will decorate the RSocket used for resonding to + * requests. + */ public InterceptorRegistry forResponder(RSocketInterceptor interceptor) { responderInterceptors.add(interceptor); return this; } + /** + * Variant of {@link #forResponder(RSocketInterceptor)} with access to the list of existing + * registrations. + */ public InterceptorRegistry forResponder(Consumer> consumer) { consumer.accept(responderInterceptors); return this; } + /** + * Add a {@link SocketAcceptorInterceptor} that will intercept the accepting of new connections. + */ public InterceptorRegistry forSocketAcceptor(SocketAcceptorInterceptor interceptor) { socketAcceptorInterceptors.add(interceptor); return this; } + /** + * Variant of {@link #forSocketAcceptor(SocketAcceptorInterceptor)} with access to the list of + * existing registrations. + */ public InterceptorRegistry forSocketAcceptor(Consumer> consumer) { consumer.accept(socketAcceptorInterceptors); return this; } - List getConnectionInterceptors() { - return connectionInterceptors; + /** Add a {@link DuplexConnectionInterceptor}. */ + public InterceptorRegistry forConnection(DuplexConnectionInterceptor interceptor) { + connectionInterceptors.add(interceptor); + return this; + } + + /** + * Variant of {@link #forConnection(DuplexConnectionInterceptor)} with access to the list of + * existing registrations. + */ + public InterceptorRegistry forConnection(Consumer> consumer) { + consumer.accept(connectionInterceptors); + return this; } List getRequesterInteceptors() { @@ -77,6 +110,10 @@ List getResponderInterceptors() { return responderInterceptors; } + List getConnectionInterceptors() { + return connectionInterceptors; + } + List getSocketAcceptorInterceptors() { return socketAcceptorInterceptors; } diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/RSocketInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/RSocketInterceptor.java index 0bad0faed..0cd4bb8f6 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/RSocketInterceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/RSocketInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -19,5 +19,10 @@ import io.rsocket.RSocket; import java.util.function.Function; -/** */ +/** + * Contract to decorate an {@link RSocket}, providing a way to intercept interactions. This can be + * applied to a {@link InterceptorRegistry#forRequester(RSocketInterceptor) requester} or {@link + * InterceptorRegistry#forResponder(RSocketInterceptor) responder} {@code RSocket} of a client or + * server. + */ public @FunctionalInterface interface RSocketInterceptor extends Function {} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java index 0cb9d92d2..6dd850ba9 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/SocketAcceptorInterceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2019 the original author or authors. + * 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. @@ -22,8 +22,8 @@ * Contract to decorate a {@link SocketAcceptor}, providing access to connection {@code setup} * information and the ability to also decorate the sockets for requesting and responding. * - *

    This can be used as an alternative to individual requester and responder {@link - * RSocketInterceptor} plugins. + *

    This could be used as an alternative to registering an individual "requester" {@code + * RSocketInterceptor} and "responder" {@code RSocketInterceptor}. */ public @FunctionalInterface interface SocketAcceptorInterceptor extends Function {} diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/package-info.java b/rsocket-core/src/main/java/io/rsocket/plugins/package-info.java new file mode 100644 index 000000000..743e3a8a4 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/plugins/package-info.java @@ -0,0 +1,18 @@ +/* + * 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. + */ + +/** Contracts for interception of transports, connections, and requests in in RSocket Java. */ +package io.rsocket.plugins; diff --git a/rsocket-core/src/main/java/io/rsocket/resume/package-info.java b/rsocket-core/src/main/java/io/rsocket/resume/package-info.java index 57027bee2..aaaa3ee9f 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/package-info.java @@ -14,5 +14,12 @@ * limitations under the License. */ +/** + * Contains support classes for the RSocket resume capability. + * + * @see Resuming + * Operation + */ @javax.annotation.ParametersAreNonnullByDefault package io.rsocket.resume; diff --git a/rsocket-core/src/main/java/io/rsocket/transport/package-info.java b/rsocket-core/src/main/java/io/rsocket/transport/package-info.java index 86e7c311a..153676324 100644 --- a/rsocket-core/src/main/java/io/rsocket/transport/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/transport/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -14,5 +14,6 @@ * limitations under the License. */ +/** Client and server transport contracts for pluggable transports. */ @javax.annotation.ParametersAreNonnullByDefault package io.rsocket.transport; diff --git a/rsocket-core/src/main/java/io/rsocket/util/package-info.java b/rsocket-core/src/main/java/io/rsocket/util/package-info.java index 79123d3b2..e034672f1 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/util/package-info.java @@ -1,5 +1,5 @@ /* - * Copyright 2015-2018 the original author or authors. + * 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. @@ -14,5 +14,6 @@ * limitations under the License. */ +/** Shared utility classes and {@link io.rsocket.Payload} implementations. */ @javax.annotation.ParametersAreNonnullByDefault package io.rsocket.util; From 79d2ee66103a73bc130c88d22c75acf1ba54df2a Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Thu, 7 May 2020 10:51:08 +0300 Subject: [PATCH 171/181] Renames Flyweight classes to Codec (#820) --- .../core/DefaultConnectionSetupPayload.java | 26 +- .../rsocket/core/PayloadValidationUtils.java | 18 +- .../io/rsocket/core/RSocketConnector.java | 4 +- .../io/rsocket/core/RSocketRequester.java | 60 ++- .../io/rsocket/core/RSocketResponder.java | 27 +- .../java/io/rsocket/core/RSocketServer.java | 14 +- .../src/main/java/io/rsocket/core/Resume.java | 4 +- .../java/io/rsocket/core/ServerSetup.java | 16 +- .../exceptions/ApplicationErrorException.java | 4 +- .../rsocket/exceptions/CanceledException.java | 4 +- .../exceptions/ConnectionCloseException.java | 4 +- .../exceptions/ConnectionErrorException.java | 4 +- .../exceptions/CustomRSocketException.java | 6 +- .../io/rsocket/exceptions/Exceptions.java | 30 +- .../rsocket/exceptions/InvalidException.java | 4 +- .../exceptions/InvalidSetupException.java | 4 +- .../rsocket/exceptions/RSocketException.java | 4 +- .../rsocket/exceptions/RejectedException.java | 4 +- .../exceptions/RejectedResumeException.java | 4 +- .../exceptions/RejectedSetupException.java | 4 +- .../io/rsocket/exceptions/SetupException.java | 4 +- .../exceptions/UnsupportedSetupException.java | 4 +- .../FragmentationDuplexConnection.java | 14 +- .../fragmentation/FrameFragmenter.java | 66 +-- .../fragmentation/FrameReassembler.java | 44 +- .../ReassemblyDuplexConnection.java | 4 +- ...meFlyweight.java => CancelFrameCodec.java} | 6 +- ...ameFlyweight.java => ErrorFrameCodec.java} | 8 +- .../main/java/io/rsocket/frame/ErrorType.java | 85 ---- .../io/rsocket/frame/ExtensionFrameCodec.java | 66 +++ .../frame/ExtensionFrameFlyweight.java | 66 --- ...Flyweight.java => FragmentationCodec.java} | 4 +- ...dataFlyweight.java => FrameBodyCodec.java} | 4 +- ...erFlyweight.java => FrameHeaderCodec.java} | 4 +- ...thFlyweight.java => FrameLengthCodec.java} | 4 +- .../main/java/io/rsocket/frame/FrameUtil.java | 44 +- .../io/rsocket/frame/GenericFrameCodec.java | 157 +++++++ ...lyweight.java => KeepAliveFrameCodec.java} | 20 +- ...ameFlyweight.java => LeaseFrameCodec.java} | 20 +- ...eight.java => MetadataPushFrameCodec.java} | 8 +- .../io/rsocket/frame/PayloadFrameCodec.java | 54 +++ .../rsocket/frame/PayloadFrameFlyweight.java | 79 ---- .../frame/RequestChannelFrameCodec.java | 67 +++ .../frame/RequestChannelFrameFlyweight.java | 81 ---- .../frame/RequestFireAndForgetFrameCodec.java | 36 ++ .../RequestFireAndForgetFrameFlyweight.java | 63 --- .../io/rsocket/frame/RequestFlyweight.java | 110 ----- ...Flyweight.java => RequestNFrameCodec.java} | 10 +- .../frame/RequestResponseFrameCodec.java | 35 ++ .../frame/RequestResponseFrameFlyweight.java | 62 --- .../frame/RequestStreamFrameCodec.java | 62 +++ .../frame/RequestStreamFrameFlyweight.java | 76 ---- ...meFlyweight.java => ResumeFrameCodec.java} | 22 +- ...Flyweight.java => ResumeOkFrameCodec.java} | 8 +- ...ameFlyweight.java => SetupFrameCodec.java} | 32 +- ...ersionFlyweight.java => VersionCodec.java} | 2 +- .../frame/decoder/DefaultPayloadDecoder.java | 38 +- .../frame/decoder/ZeroCopyPayloadDecoder.java | 38 +- .../ClientServerInputMultiplexer.java | 6 +- .../rsocket/keepalive/KeepAliveSupport.java | 12 +- .../rsocket/lease/RequesterLeaseHandler.java | 8 +- .../rsocket/lease/ResponderLeaseHandler.java | 4 +- .../rsocket/metadata/AuthMetadataCodec.java | 335 +++++++++++++++ .../metadata/CompositeMetadataCodec.java | 385 ++++++++++++++++++ .../metadata/CompositeMetadataFlyweight.java | 169 ++------ .../metadata/TaggingMetadataCodec.java | 76 ++++ .../metadata/TaggingMetadataFlyweight.java | 26 +- .../rsocket/metadata/WellKnownAuthType.java | 121 ++++++ .../security/AuthMetadataFlyweight.java | 175 +------- .../metadata/security/WellKnownAuthType.java | 26 ++ .../rsocket/resume/ClientRSocketSession.java | 12 +- .../resume/ResumableDuplexConnection.java | 4 +- .../rsocket/resume/ServerRSocketSession.java | 16 +- .../core/ConnectionSetupPayloadTest.java | 8 +- .../java/io/rsocket/core/KeepAliveTest.java | 14 +- .../core/PayloadValidationUtilsTest.java | 34 +- .../io/rsocket/core/RSocketLeaseTest.java | 24 +- .../core/RSocketRequesterSubscribersTest.java | 12 +- .../io/rsocket/core/RSocketRequesterTest.java | 112 +++-- .../io/rsocket/core/RSocketResponderTest.java | 90 ++-- .../io/rsocket/core/SetupRejectionTest.java | 16 +- .../io/rsocket/exceptions/ExceptionsTest.java | 28 +- .../FragmentationDuplexConnectionTest.java | 6 +- .../FragmentationIntegrationTest.java | 13 +- .../fragmentation/FrameFragmenterTest.java | 131 +++--- .../fragmentation/FrameReassemblerTest.java | 88 ++-- .../ReassembleDuplexConnectionTest.java | 55 ++- ...ightTest.java => ErrorFrameCodecTest.java} | 6 +- .../frame/ExtensionFrameCodecTest.java | 62 +++ .../frame/ExtensionFrameFlyweightTest.java | 62 --- ...ghtTest.java => FrameHeaderCodecTest.java} | 16 +- ...htTest.java => GenericFrameCodecTest.java} | 96 +++-- .../frame/KeepaliveFrameFlyweightTest.java | 10 +- .../io/rsocket/frame/LeaseFrameCodecTest.java | 42 ++ .../frame/LeaseFrameFlyweightTest.java | 43 -- .../rsocket/frame/PayloadFlyweightTest.java | 39 +- ...tTest.java => RequestNFrameCodecTest.java} | 6 +- ...ghtTest.java => ResumeFrameCodecTest.java} | 13 +- ...tTest.java => ResumeOkFrameCodecTest.java} | 6 +- .../io/rsocket/frame/SetupFrameCodecTest.java | 57 +++ .../frame/SetupFrameFlyweightTest.java | 57 --- ...yweightTest.java => VersionCodecTest.java} | 24 +- .../ClientServerInputMultiplexerTest.java | 14 +- .../MicrometerDuplexConnection.java | 4 +- .../main/java/io/rsocket/test/TestFrames.java | 28 +- .../transport/netty/RSocketLengthCodec.java | 4 +- .../transport/netty/TcpDuplexConnection.java | 6 +- .../client/WebsocketClientTransport.java | 2 +- .../netty/server/WebsocketRouteTransport.java | 2 +- .../server/WebsocketServerTransport.java | 2 +- .../client/WebsocketClientTransportTest.java | 2 +- .../server/WebsocketServerTransportTest.java | 2 +- 112 files changed, 2422 insertions(+), 1941 deletions(-) rename rsocket-core/src/main/java/io/rsocket/frame/{CancelFrameFlyweight.java => CancelFrameCodec.java} (55%) rename rsocket-core/src/main/java/io/rsocket/frame/{ErrorFrameFlyweight.java => ErrorFrameCodec.java} (89%) delete mode 100644 rsocket-core/src/main/java/io/rsocket/frame/ErrorType.java create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameCodec.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameFlyweight.java rename rsocket-core/src/main/java/io/rsocket/frame/{FragmentationFlyweight.java => FragmentationCodec.java} (80%) rename rsocket-core/src/main/java/io/rsocket/frame/{DataAndMetadataFlyweight.java => FrameBodyCodec.java} (97%) rename rsocket-core/src/main/java/io/rsocket/frame/{FrameHeaderFlyweight.java => FrameHeaderCodec.java} (98%) rename rsocket-core/src/main/java/io/rsocket/frame/{FrameLengthFlyweight.java => FrameLengthCodec.java} (95%) create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/GenericFrameCodec.java rename rsocket-core/src/main/java/io/rsocket/frame/{KeepAliveFrameFlyweight.java => KeepAliveFrameCodec.java} (61%) rename rsocket-core/src/main/java/io/rsocket/frame/{LeaseFrameFlyweight.java => LeaseFrameCodec.java} (74%) rename rsocket-core/src/main/java/io/rsocket/frame/{MetadataPushFrameFlyweight.java => MetadataPushFrameCodec.java} (83%) create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameCodec.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameCodec.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameCodec.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/frame/RequestFlyweight.java rename rsocket-core/src/main/java/io/rsocket/frame/{RequestNFrameFlyweight.java => RequestNFrameCodec.java} (68%) create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameCodec.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameCodec.java delete mode 100644 rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java rename rsocket-core/src/main/java/io/rsocket/frame/{ResumeFrameFlyweight.java => ResumeFrameCodec.java} (79%) rename rsocket-core/src/main/java/io/rsocket/frame/{ResumeOkFrameFlyweight.java => ResumeOkFrameCodec.java} (67%) rename rsocket-core/src/main/java/io/rsocket/frame/{SetupFrameFlyweight.java => SetupFrameCodec.java} (83%) rename rsocket-core/src/main/java/io/rsocket/frame/{VersionFlyweight.java => VersionCodec.java} (96%) create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataCodec.java create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataCodec.java create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/WellKnownAuthType.java rename rsocket-core/src/test/java/io/rsocket/frame/{ErrorFrameFlyweightTest.java => ErrorFrameCodecTest.java} (65%) create mode 100644 rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameCodecTest.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameFlyweightTest.java rename rsocket-core/src/test/java/io/rsocket/frame/{FrameHeaderFlyweightTest.java => FrameHeaderCodecTest.java} (52%) rename rsocket-core/src/test/java/io/rsocket/frame/{RequestFlyweightTest.java => GenericFrameCodecTest.java} (65%) create mode 100644 rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameCodecTest.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameFlyweightTest.java rename rsocket-core/src/test/java/io/rsocket/frame/{RequestNFrameFlyweightTest.java => RequestNFrameCodecTest.java} (63%) rename rsocket-core/src/test/java/io/rsocket/frame/{ResumeFrameFlyweightTest.java => ResumeFrameCodecTest.java} (68%) rename rsocket-core/src/test/java/io/rsocket/frame/{ResumeOkFrameFlyweightTest.java => ResumeOkFrameCodecTest.java} (51%) create mode 100644 rsocket-core/src/test/java/io/rsocket/frame/SetupFrameCodecTest.java delete mode 100644 rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java rename rsocket-core/src/test/java/io/rsocket/frame/{VersionFlyweightTest.java => VersionCodecTest.java} (58%) diff --git a/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java b/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java index feeb5c481..9b5647c6f 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/core/DefaultConnectionSetupPayload.java @@ -19,8 +19,8 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.ConnectionSetupPayload; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.SetupFrameCodec; /** * Default implementation of {@link ConnectionSetupPayload}. Primarily for internal use within @@ -36,18 +36,18 @@ public DefaultConnectionSetupPayload(ByteBuf setupFrame) { @Override public boolean hasMetadata() { - return FrameHeaderFlyweight.hasMetadata(setupFrame); + return FrameHeaderCodec.hasMetadata(setupFrame); } @Override public ByteBuf sliceMetadata() { - final ByteBuf metadata = SetupFrameFlyweight.metadata(setupFrame); + final ByteBuf metadata = SetupFrameCodec.metadata(setupFrame); return metadata == null ? Unpooled.EMPTY_BUFFER : metadata; } @Override public ByteBuf sliceData() { - return SetupFrameFlyweight.data(setupFrame); + return SetupFrameCodec.data(setupFrame); } @Override @@ -62,42 +62,42 @@ public ByteBuf metadata() { @Override public String metadataMimeType() { - return SetupFrameFlyweight.metadataMimeType(setupFrame); + return SetupFrameCodec.metadataMimeType(setupFrame); } @Override public String dataMimeType() { - return SetupFrameFlyweight.dataMimeType(setupFrame); + return SetupFrameCodec.dataMimeType(setupFrame); } @Override public int keepAliveInterval() { - return SetupFrameFlyweight.keepAliveInterval(setupFrame); + return SetupFrameCodec.keepAliveInterval(setupFrame); } @Override public int keepAliveMaxLifetime() { - return SetupFrameFlyweight.keepAliveMaxLifetime(setupFrame); + return SetupFrameCodec.keepAliveMaxLifetime(setupFrame); } @Override public int getFlags() { - return FrameHeaderFlyweight.flags(setupFrame); + return FrameHeaderCodec.flags(setupFrame); } @Override public boolean willClientHonorLease() { - return SetupFrameFlyweight.honorLease(setupFrame); + return SetupFrameCodec.honorLease(setupFrame); } @Override public boolean isResumeEnabled() { - return SetupFrameFlyweight.resumeEnabled(setupFrame); + return SetupFrameCodec.resumeEnabled(setupFrame); } @Override public ByteBuf resumeToken() { - return SetupFrameFlyweight.resumeToken(setupFrame); + return SetupFrameCodec.resumeToken(setupFrame); } @Override diff --git a/rsocket-core/src/main/java/io/rsocket/core/PayloadValidationUtils.java b/rsocket-core/src/main/java/io/rsocket/core/PayloadValidationUtils.java index 3b6b375d1..2d2b96f7e 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/PayloadValidationUtils.java +++ b/rsocket-core/src/main/java/io/rsocket/core/PayloadValidationUtils.java @@ -1,8 +1,8 @@ package io.rsocket.core; import io.rsocket.Payload; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameLengthCodec; final class PayloadValidationUtils { static final String INVALID_PAYLOAD_ERROR_MESSAGE = @@ -14,18 +14,18 @@ static boolean isValid(int mtu, Payload payload) { } if (payload.hasMetadata()) { - return (((FrameHeaderFlyweight.size() - + FrameLengthFlyweight.FRAME_LENGTH_SIZE - + FrameHeaderFlyweight.size() + return (((FrameHeaderCodec.size() + + FrameLengthCodec.FRAME_LENGTH_SIZE + + FrameHeaderCodec.size() + payload.data().readableBytes() + payload.metadata().readableBytes()) - & ~FrameLengthFlyweight.FRAME_LENGTH_MASK) + & ~FrameLengthCodec.FRAME_LENGTH_MASK) == 0); } else { - return (((FrameHeaderFlyweight.size() + return (((FrameHeaderCodec.size() + payload.data().readableBytes() - + FrameLengthFlyweight.FRAME_LENGTH_SIZE) - & ~FrameLengthFlyweight.FRAME_LENGTH_MASK) + + FrameLengthCodec.FRAME_LENGTH_SIZE) + & ~FrameLengthCodec.FRAME_LENGTH_MASK) == 0); } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 45d16a665..b69610f3f 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -23,7 +23,7 @@ import io.rsocket.RSocket; import io.rsocket.SocketAcceptor; import io.rsocket.fragmentation.FragmentationDuplexConnection; -import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.frame.SetupFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.ClientServerInputMultiplexer; import io.rsocket.keepalive.KeepAliveHandler; @@ -536,7 +536,7 @@ public Mono connect(Supplier transportSupplier) { RSocket wrappedRSocketRequester = interceptors.initRequester(rSocketRequester); ByteBuf setupFrame = - SetupFrameFlyweight.encode( + SetupFrameCodec.encode( wrappedConnection.alloc(), leaseEnabled, (int) keepAliveInterval.toMillis(), diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index a2bd3d9fd..846eaa922 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -31,17 +31,17 @@ import io.rsocket.RSocket; import io.rsocket.exceptions.ConnectionErrorException; import io.rsocket.exceptions.Exceptions; -import io.rsocket.frame.CancelFrameFlyweight; -import io.rsocket.frame.ErrorFrameFlyweight; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.CancelFrameCodec; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; -import io.rsocket.frame.MetadataPushFrameFlyweight; -import io.rsocket.frame.PayloadFrameFlyweight; -import io.rsocket.frame.RequestChannelFrameFlyweight; -import io.rsocket.frame.RequestFireAndForgetFrameFlyweight; -import io.rsocket.frame.RequestNFrameFlyweight; -import io.rsocket.frame.RequestResponseFrameFlyweight; -import io.rsocket.frame.RequestStreamFrameFlyweight; +import io.rsocket.frame.MetadataPushFrameCodec; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestNFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.SynchronizedIntObjectHashMap; import io.rsocket.internal.UnboundedProcessor; @@ -225,7 +225,7 @@ private Mono handleFireAndForget(Payload payload) { final int streamId = streamIdSupplier.nextStreamId(receivers); final ByteBuf requestFrame = - RequestFireAndForgetFrameFlyweight.encodeReleasingPayload( + RequestFireAndForgetFrameCodec.encodeReleasingPayload( allocator, streamId, payload); sendProcessor.onNext(requestFrame); @@ -275,7 +275,7 @@ void hookOnFirstRequest(long n) { this.streamId = streamId; ByteBuf requestResponseFrame = - RequestResponseFrameFlyweight.encodeReleasingPayload( + RequestResponseFrameCodec.encodeReleasingPayload( allocator, streamId, payload); receivers.put(streamId, receiver); @@ -285,8 +285,7 @@ void hookOnFirstRequest(long n) { @Override void hookOnCancel() { if (receivers.remove(streamId, receiver)) { - sendProcessor.onNext( - CancelFrameFlyweight.encode(allocator, streamId)); + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); } else { payload.release(); } @@ -341,7 +340,7 @@ void hookOnFirstRequest(long n) { this.streamId = streamId; ByteBuf requestStreamFrame = - RequestStreamFrameFlyweight.encodeReleasingPayload( + RequestStreamFrameCodec.encodeReleasingPayload( allocator, streamId, n, payload); receivers.put(streamId, receiver); @@ -356,14 +355,13 @@ void hookOnRemainingRequests(long n) { } sendProcessor.onNext( - RequestNFrameFlyweight.encode(allocator, streamId, n)); + RequestNFrameCodec.encode(allocator, streamId, n)); } @Override void hookOnCancel() { if (receivers.remove(streamId, receiver)) { - sendProcessor.onNext( - CancelFrameFlyweight.encode(allocator, streamId)); + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); } else { payload.release(); } @@ -450,13 +448,12 @@ protected void hookOnNext(Payload payload) { new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); errorConsumer.accept(t); // no need to send any errors. - sendProcessor.onNext( - CancelFrameFlyweight.encode(allocator, streamId)); + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); receiver.onError(t); return; } final ByteBuf frame = - PayloadFrameFlyweight.encodeNextReleasingPayload( + PayloadFrameCodec.encodeNextReleasingPayload( allocator, streamId, payload); sendProcessor.onNext(frame); @@ -464,14 +461,13 @@ protected void hookOnNext(Payload payload) { @Override protected void hookOnComplete() { - ByteBuf frame = - PayloadFrameFlyweight.encodeComplete(allocator, streamId); + ByteBuf frame = PayloadFrameCodec.encodeComplete(allocator, streamId); sendProcessor.onNext(frame); } @Override protected void hookOnError(Throwable t) { - ByteBuf frame = ErrorFrameFlyweight.encode(allocator, streamId, t); + ByteBuf frame = ErrorFrameCodec.encode(allocator, streamId, t); sendProcessor.onNext(frame); receiver.onError(t); } @@ -488,7 +484,7 @@ void hookOnFirstRequest(long n) { this.streamId = streamId; final ByteBuf frame = - RequestChannelFrameFlyweight.encodeReleasingPayload( + RequestChannelFrameCodec.encodeReleasingPayload( allocator, streamId, false, n, initialPayload); senders.put(streamId, upstreamSubscriber); @@ -508,14 +504,14 @@ void hookOnRemainingRequests(long n) { return; } - sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); + sendProcessor.onNext(RequestNFrameCodec.encode(allocator, streamId, n)); } @Override void hookOnCancel() { senders.remove(streamId, upstreamSubscriber); if (receivers.remove(streamId, receiver)) { - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); } } @@ -562,7 +558,7 @@ private Mono handleMetadataPush(Payload payload) { } ByteBuf metadataPushFrame = - MetadataPushFrameFlyweight.encodeReleasingPayload(allocator, payload); + MetadataPushFrameCodec.encodeReleasingPayload(allocator, payload); sendProcessor.onNextPrioritized(metadataPushFrame); @@ -585,8 +581,8 @@ private Throwable checkAvailable() { private void handleIncomingFrames(ByteBuf frame) { try { - int streamId = FrameHeaderFlyweight.streamId(frame); - FrameType type = FrameHeaderFlyweight.frameType(frame); + int streamId = FrameHeaderCodec.streamId(frame); + FrameType type = FrameHeaderCodec.frameType(frame); if (streamId == 0) { handleStreamZero(type, frame); } else { @@ -666,7 +662,7 @@ private void handleFrame(int streamId, FrameType type, ByteBuf frame) { { Subscription sender = senders.get(streamId); if (sender != null) { - long n = RequestNFrameFlyweight.requestN(frame); + long n = RequestNFrameCodec.requestN(frame); sender.request(n); } break; @@ -682,7 +678,7 @@ private void handleMissingResponseProcessor(int streamId, FrameType type, ByteBu if (type == FrameType.ERROR) { // message for stream that has never existed, we have a problem with // the overall connection and must tear down - String errorMessage = ErrorFrameFlyweight.dataUtf8(frame); + String errorMessage = ErrorFrameCodec.dataUtf8(frame); throw new IllegalStateException( "Client received error for non-existent stream: " diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index 2f073ba8a..b9d3ea794 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -292,9 +292,9 @@ private synchronized void cleanUpChannelProcessors(Throwable e) { private void handleFrame(ByteBuf frame) { try { - int streamId = FrameHeaderFlyweight.streamId(frame); + int streamId = FrameHeaderCodec.streamId(frame); Subscriber receiver; - FrameType frameType = FrameHeaderFlyweight.frameType(frame); + FrameType frameType = FrameHeaderCodec.frameType(frame); switch (frameType) { case REQUEST_FNF: handleFireAndForget(streamId, fireAndForget(payloadDecoder.apply(frame))); @@ -309,12 +309,12 @@ private void handleFrame(ByteBuf frame) { handleRequestN(streamId, frame); break; case REQUEST_STREAM: - long streamInitialRequestN = RequestStreamFrameFlyweight.initialRequestN(frame); + long streamInitialRequestN = RequestStreamFrameCodec.initialRequestN(frame); Payload streamPayload = payloadDecoder.apply(frame); handleStream(streamId, requestStream(streamPayload), streamInitialRequestN, null); break; case REQUEST_CHANNEL: - long channelInitialRequestN = RequestChannelFrameFlyweight.initialRequestN(frame); + long channelInitialRequestN = RequestChannelFrameCodec.initialRequestN(frame); Payload channelPayload = payloadDecoder.apply(frame); handleChannel(streamId, channelPayload, channelInitialRequestN); break; @@ -339,7 +339,7 @@ private void handleFrame(ByteBuf frame) { case ERROR: receiver = channelProcessors.get(streamId); if (receiver != null) { - receiver.onError(new ApplicationErrorException(ErrorFrameFlyweight.dataUtf8(frame))); + receiver.onError(new ApplicationErrorException(ErrorFrameCodec.dataUtf8(frame))); } break; case NEXT_COMPLETE: @@ -408,8 +408,7 @@ protected void hookOnNext(Payload payload) { } ByteBuf byteBuf = - PayloadFrameFlyweight.encodeNextCompleteReleasingPayload( - allocator, streamId, payload); + PayloadFrameCodec.encodeNextCompleteReleasingPayload(allocator, streamId, payload); sendProcessor.onNext(byteBuf); } @@ -421,7 +420,7 @@ protected void hookOnError(Throwable throwable) { @Override protected void hookOnComplete() { if (isEmpty) { - sendProcessor.onNext(PayloadFrameFlyweight.encodeComplete(allocator, streamId)); + sendProcessor.onNext(PayloadFrameCodec.encodeComplete(allocator, streamId)); } } @@ -473,7 +472,7 @@ protected void hookOnNext(Payload payload) { } ByteBuf byteBuf = - PayloadFrameFlyweight.encodeNextReleasingPayload(allocator, streamId, payload); + PayloadFrameCodec.encodeNextReleasingPayload(allocator, streamId, payload); sendProcessor.onNext(byteBuf); } catch (Throwable e) { // specifically for requestChannel case so when Payload is invalid we will not be @@ -494,7 +493,7 @@ protected void hookOnNext(Payload payload) { @Override protected void hookOnComplete() { - sendProcessor.onNext(PayloadFrameFlyweight.encodeComplete(allocator, streamId)); + sendProcessor.onNext(PayloadFrameCodec.encodeComplete(allocator, streamId)); } @Override @@ -553,7 +552,7 @@ public void accept(long l) { n = l; } if (n > 0) { - sendProcessor.onNext(RequestNFrameFlyweight.encode(allocator, streamId, n)); + sendProcessor.onNext(RequestNFrameCodec.encode(allocator, streamId, n)); } } }) @@ -561,7 +560,7 @@ public void accept(long l) { signalType -> { if (channelProcessors.remove(streamId, frames)) { if (signalType == SignalType.CANCEL) { - sendProcessor.onNext(CancelFrameFlyweight.encode(allocator, streamId)); + sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); } } }) @@ -605,14 +604,14 @@ private void handleCancelFrame(int streamId) { private void handleError(int streamId, Throwable t) { errorConsumer.accept(t); - sendProcessor.onNext(ErrorFrameFlyweight.encode(allocator, streamId, t)); + sendProcessor.onNext(ErrorFrameCodec.encode(allocator, streamId, t)); } private void handleRequestN(int streamId, ByteBuf frame) { Subscription subscription = sendingSubscriptions.get(streamId); if (subscription != null) { - long n = RequestNFrameFlyweight.requestN(frame); + long n = RequestNFrameCodec.requestN(frame); subscription.request(n); } } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index 5960e33d4..66e2249b5 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -26,8 +26,8 @@ import io.rsocket.exceptions.InvalidSetupException; import io.rsocket.exceptions.RejectedSetupException; import io.rsocket.fragmentation.FragmentationDuplexConnection; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.SetupFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.ClientServerInputMultiplexer; import io.rsocket.lease.Leases; @@ -325,7 +325,7 @@ private Mono acceptResume( private Mono accept( ServerSetup serverSetup, ByteBuf startFrame, ClientServerInputMultiplexer multiplexer) { - switch (FrameHeaderFlyweight.frameType(startFrame)) { + switch (FrameHeaderCodec.frameType(startFrame)) { case SETUP: return acceptSetup(serverSetup, startFrame, multiplexer); case RESUME: @@ -335,7 +335,7 @@ private Mono accept( .sendError( multiplexer, new InvalidSetupException( - "invalid setup frame: " + FrameHeaderFlyweight.frameType(startFrame))) + "invalid setup frame: " + FrameHeaderCodec.frameType(startFrame))) .doFinally( signalType -> { startFrame.release(); @@ -347,12 +347,12 @@ private Mono accept( private Mono acceptSetup( ServerSetup serverSetup, ByteBuf setupFrame, ClientServerInputMultiplexer multiplexer) { - if (!SetupFrameFlyweight.isSupportedVersion(setupFrame)) { + if (!SetupFrameCodec.isSupportedVersion(setupFrame)) { return serverSetup .sendError( multiplexer, new InvalidSetupException( - "Unsupported version: " + SetupFrameFlyweight.humanReadableVersion(setupFrame))) + "Unsupported version: " + SetupFrameCodec.humanReadableVersion(setupFrame))) .doFinally( signalType -> { setupFrame.release(); @@ -361,7 +361,7 @@ private Mono acceptSetup( } boolean leaseEnabled = leasesSupplier != null; - if (SetupFrameFlyweight.honorLease(setupFrame) && !leaseEnabled) { + if (SetupFrameCodec.honorLease(setupFrame) && !leaseEnabled) { return serverSetup .sendError(multiplexer, new InvalidSetupException("lease is not supported")) .doFinally( diff --git a/rsocket-core/src/main/java/io/rsocket/core/Resume.java b/rsocket-core/src/main/java/io/rsocket/core/Resume.java index 04221154f..48133af98 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/Resume.java +++ b/rsocket-core/src/main/java/io/rsocket/core/Resume.java @@ -16,7 +16,7 @@ package io.rsocket.core; import io.netty.buffer.ByteBuf; -import io.rsocket.frame.ResumeFrameFlyweight; +import io.rsocket.frame.ResumeFrameCodec; import io.rsocket.resume.InMemoryResumableFramesStore; import io.rsocket.resume.ResumableFramesStore; import java.time.Duration; @@ -43,7 +43,7 @@ public class Resume { private Duration streamTimeout = Duration.ofSeconds(10); /* Client only */ - private Supplier tokenSupplier = ResumeFrameFlyweight::generateResumeToken; + private Supplier tokenSupplier = ResumeFrameCodec::generateResumeToken; private Retry retry = Retry.backoff(Long.MAX_VALUE, Duration.ofSeconds(1)) .maxBackoff(Duration.ofSeconds(16)) diff --git a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java index 3e20d3c60..337d17c64 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java +++ b/rsocket-core/src/main/java/io/rsocket/core/ServerSetup.java @@ -22,9 +22,9 @@ import io.rsocket.DuplexConnection; import io.rsocket.exceptions.RejectedResumeException; import io.rsocket.exceptions.UnsupportedSetupException; -import io.rsocket.frame.ErrorFrameFlyweight; -import io.rsocket.frame.ResumeFrameFlyweight; -import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.ResumeFrameCodec; +import io.rsocket.frame.SetupFrameCodec; import io.rsocket.internal.ClientServerInputMultiplexer; import io.rsocket.keepalive.KeepAliveHandler; import io.rsocket.resume.*; @@ -47,7 +47,7 @@ void dispose() {} Mono sendError(ClientServerInputMultiplexer multiplexer, Exception exception) { DuplexConnection duplexConnection = multiplexer.asSetupConnection(); return duplexConnection - .sendOne(ErrorFrameFlyweight.encode(duplexConnection.alloc(), 0, exception)) + .sendOne(ErrorFrameCodec.encode(duplexConnection.alloc(), 0, exception)) .onErrorResume(err -> Mono.empty()); } @@ -59,7 +59,7 @@ public Mono acceptRSocketSetup( ClientServerInputMultiplexer multiplexer, BiFunction> then) { - if (SetupFrameFlyweight.resumeEnabled(frame)) { + if (SetupFrameCodec.resumeEnabled(frame)) { return sendError(multiplexer, new UnsupportedSetupException("resume not supported")) .doFinally( signalType -> { @@ -109,8 +109,8 @@ public Mono acceptRSocketSetup( ClientServerInputMultiplexer multiplexer, BiFunction> then) { - if (SetupFrameFlyweight.resumeEnabled(frame)) { - ByteBuf resumeToken = SetupFrameFlyweight.resumeToken(frame); + if (SetupFrameCodec.resumeEnabled(frame)) { + ByteBuf resumeToken = SetupFrameCodec.resumeToken(frame); ResumableDuplexConnection connection = sessionManager @@ -133,7 +133,7 @@ public Mono acceptRSocketSetup( @Override public Mono acceptRSocketResume(ByteBuf frame, ClientServerInputMultiplexer multiplexer) { - ServerRSocketSession session = sessionManager.get(ResumeFrameFlyweight.token(frame)); + ServerRSocketSession session = sessionManager.get(ResumeFrameCodec.token(frame)); if (session != null) { return session .continueWith(multiplexer.asClientServerConnection()) diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java index 351e045a3..e617b82d8 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import javax.annotation.Nullable; /** @@ -45,6 +45,6 @@ public ApplicationErrorException(String message) { * @param cause the cause of this exception */ public ApplicationErrorException(String message, @Nullable Throwable cause) { - super(ErrorFrameFlyweight.APPLICATION_ERROR, message, cause); + super(ErrorFrameCodec.APPLICATION_ERROR, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java index 537cf2bf2..3c5fc7420 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import javax.annotation.Nullable; /** @@ -46,6 +46,6 @@ public CanceledException(String message) { * @param cause the cause of this exception */ public CanceledException(String message, @Nullable Throwable cause) { - super(ErrorFrameFlyweight.CANCELED, message, cause); + super(ErrorFrameCodec.CANCELED, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java index f1f1a47d8..5cff2c821 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import javax.annotation.Nullable; /** @@ -46,6 +46,6 @@ public ConnectionCloseException(String message) { * @param cause the cause of this exception */ public ConnectionCloseException(String message, @Nullable Throwable cause) { - super(ErrorFrameFlyweight.CONNECTION_CLOSE, message, cause); + super(ErrorFrameCodec.CONNECTION_CLOSE, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java index 9581cfc97..3fcb8f5de 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import javax.annotation.Nullable; /** @@ -46,6 +46,6 @@ public ConnectionErrorException(String message) { * @param cause the cause of this exception */ public ConnectionErrorException(String message, @Nullable Throwable cause) { - super(ErrorFrameFlyweight.CONNECTION_ERROR, message, cause); + super(ErrorFrameCodec.CONNECTION_ERROR, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java index 5c1154ebd..18f488ba0 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import javax.annotation.Nullable; public class CustomRSocketException extends RSocketException { @@ -43,8 +43,8 @@ public CustomRSocketException(int errorCode, String message) { */ public CustomRSocketException(int errorCode, String message, @Nullable Throwable cause) { super(errorCode, message, cause); - if (errorCode > ErrorFrameFlyweight.MAX_USER_ALLOWED_ERROR_CODE - && errorCode < ErrorFrameFlyweight.MIN_USER_ALLOWED_ERROR_CODE) { + if (errorCode > ErrorFrameCodec.MAX_USER_ALLOWED_ERROR_CODE + && errorCode < ErrorFrameCodec.MIN_USER_ALLOWED_ERROR_CODE) { throw new IllegalArgumentException( "Allowed errorCode value should be in range [0x00000301-0xFFFFFFFE]", this); } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java b/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java index fe2d304f5..5c6eee614 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/Exceptions.java @@ -16,22 +16,22 @@ package io.rsocket.exceptions; -import static io.rsocket.frame.ErrorFrameFlyweight.APPLICATION_ERROR; -import static io.rsocket.frame.ErrorFrameFlyweight.CANCELED; -import static io.rsocket.frame.ErrorFrameFlyweight.CONNECTION_CLOSE; -import static io.rsocket.frame.ErrorFrameFlyweight.CONNECTION_ERROR; -import static io.rsocket.frame.ErrorFrameFlyweight.INVALID; -import static io.rsocket.frame.ErrorFrameFlyweight.INVALID_SETUP; -import static io.rsocket.frame.ErrorFrameFlyweight.MAX_USER_ALLOWED_ERROR_CODE; -import static io.rsocket.frame.ErrorFrameFlyweight.MIN_USER_ALLOWED_ERROR_CODE; -import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED; -import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED_RESUME; -import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED_SETUP; -import static io.rsocket.frame.ErrorFrameFlyweight.UNSUPPORTED_SETUP; +import static io.rsocket.frame.ErrorFrameCodec.APPLICATION_ERROR; +import static io.rsocket.frame.ErrorFrameCodec.CANCELED; +import static io.rsocket.frame.ErrorFrameCodec.CONNECTION_CLOSE; +import static io.rsocket.frame.ErrorFrameCodec.CONNECTION_ERROR; +import static io.rsocket.frame.ErrorFrameCodec.INVALID; +import static io.rsocket.frame.ErrorFrameCodec.INVALID_SETUP; +import static io.rsocket.frame.ErrorFrameCodec.MAX_USER_ALLOWED_ERROR_CODE; +import static io.rsocket.frame.ErrorFrameCodec.MIN_USER_ALLOWED_ERROR_CODE; +import static io.rsocket.frame.ErrorFrameCodec.REJECTED; +import static io.rsocket.frame.ErrorFrameCodec.REJECTED_RESUME; +import static io.rsocket.frame.ErrorFrameCodec.REJECTED_SETUP; +import static io.rsocket.frame.ErrorFrameCodec.UNSUPPORTED_SETUP; import io.netty.buffer.ByteBuf; import io.rsocket.RSocketErrorException; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import java.util.Objects; /** Utility class that generates an exception from a frame. */ @@ -49,8 +49,8 @@ private Exceptions() {} public static RuntimeException from(int streamId, ByteBuf frame) { Objects.requireNonNull(frame, "frame must not be null"); - int errorCode = ErrorFrameFlyweight.errorCode(frame); - String message = ErrorFrameFlyweight.dataUtf8(frame); + int errorCode = ErrorFrameCodec.errorCode(frame); + String message = ErrorFrameCodec.dataUtf8(frame); if (streamId == 0) { switch (errorCode) { diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java index a4b28659f..2428d1e7e 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import javax.annotation.Nullable; /** @@ -45,6 +45,6 @@ public InvalidException(String message) { * @param cause the cause of this exception */ public InvalidException(String message, @Nullable Throwable cause) { - super(ErrorFrameFlyweight.INVALID, message, cause); + super(ErrorFrameCodec.INVALID, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java index 1ff53d51d..57da19bb6 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import javax.annotation.Nullable; /** @@ -46,6 +46,6 @@ public InvalidSetupException(String message) { * @param cause the cause of this exception */ public InvalidSetupException(String message, @Nullable Throwable cause) { - super(ErrorFrameFlyweight.INVALID_SETUP, message, cause); + super(ErrorFrameCodec.INVALID_SETUP, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RSocketException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RSocketException.java index 93c49d5e2..2b137282f 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RSocketException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RSocketException.java @@ -17,7 +17,7 @@ package io.rsocket.exceptions; import io.rsocket.RSocketErrorException; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import reactor.util.annotation.Nullable; /** @@ -47,7 +47,7 @@ public RSocketException(String message) { * @param cause the cause of this exception */ public RSocketException(String message, @Nullable Throwable cause) { - super(ErrorFrameFlyweight.APPLICATION_ERROR, message, cause); + super(ErrorFrameCodec.APPLICATION_ERROR, message, cause); } /** diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java index 3fad3f396..c87a60243 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import javax.annotation.Nullable; /** @@ -47,6 +47,6 @@ public RejectedException(String message) { * @param cause the cause of this exception */ public RejectedException(String message, @Nullable Throwable cause) { - super(ErrorFrameFlyweight.REJECTED, message, cause); + super(ErrorFrameCodec.REJECTED, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java index a10eb4197..8a6ea2244 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import javax.annotation.Nullable; /** @@ -45,6 +45,6 @@ public RejectedResumeException(String message) { * @param cause the cause of this exception */ public RejectedResumeException(String message, @Nullable Throwable cause) { - super(ErrorFrameFlyweight.REJECTED_RESUME, message, cause); + super(ErrorFrameCodec.REJECTED_RESUME, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java index 6b5dc0f8b..972b430a7 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import javax.annotation.Nullable; /** @@ -45,6 +45,6 @@ public RejectedSetupException(String message) { * @param cause the cause of this exception */ public RejectedSetupException(String message, @Nullable Throwable cause) { - super(ErrorFrameFlyweight.REJECTED_SETUP, message, cause); + super(ErrorFrameCodec.REJECTED_SETUP, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java index 712508f0b..158e5410d 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import javax.annotation.Nullable; /** The root of the setup exception hierarchy. */ @@ -44,7 +44,7 @@ public SetupException(String message) { */ @Deprecated public SetupException(String message, @Nullable Throwable cause) { - this(ErrorFrameFlyweight.INVALID_SETUP, message, cause); + this(ErrorFrameCodec.INVALID_SETUP, message, cause); } /** diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java index b112b95be..3282c9750 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java @@ -16,7 +16,7 @@ package io.rsocket.exceptions; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import javax.annotation.Nullable; /** @@ -45,6 +45,6 @@ public UnsupportedSetupException(String message) { * @param cause the cause of this exception */ public UnsupportedSetupException(String message, @Nullable Throwable cause) { - super(ErrorFrameFlyweight.UNSUPPORTED_SETUP, message, cause); + super(ErrorFrameCodec.UNSUPPORTED_SETUP, message, cause); } } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java index 24b360755..5192ffead 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java @@ -21,8 +21,8 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; import io.rsocket.DuplexConnection; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameLengthCodec; import io.rsocket.frame.FrameType; import java.util.Objects; import javax.annotation.Nullable; @@ -100,7 +100,7 @@ public Mono send(Publisher frames) { @Override public Mono sendOne(ByteBuf frame) { - FrameType frameType = FrameHeaderFlyweight.frameType(frame); + FrameType frameType = FrameHeaderCodec.frameType(frame); int readableBytes = frame.readableBytes(); if (shouldFragment(frameType, readableBytes)) { if (logger.isDebugEnabled()) { @@ -108,12 +108,12 @@ public Mono sendOne(ByteBuf frame) { Flux.from(fragmentFrame(alloc(), mtu, frame, frameType, encodeLength)) .doOnNext( byteBuf -> { - ByteBuf f = encodeLength ? FrameLengthFlyweight.frame(byteBuf) : byteBuf; + ByteBuf f = encodeLength ? FrameLengthCodec.frame(byteBuf) : byteBuf; logger.debug( "{} - stream id {} - frame type {} - \n {}", type, - FrameHeaderFlyweight.streamId(f), - FrameHeaderFlyweight.frameType(f), + FrameHeaderCodec.streamId(f), + FrameHeaderCodec.frameType(f), ByteBufUtil.prettyHexDump(f)); })); } else { @@ -127,7 +127,7 @@ public Mono sendOne(ByteBuf frame) { private ByteBuf encode(ByteBuf frame) { if (encodeLength) { - return FrameLengthFlyweight.encode(alloc(), frame.readableBytes(), frame); + return FrameLengthCodec.encode(alloc(), frame.readableBytes(), frame); } else { return frame; } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java index 8593d2be7..4b8fd36e9 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameFragmenter.java @@ -20,14 +20,14 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; import io.netty.util.ReferenceCountUtil; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameLengthCodec; import io.rsocket.frame.FrameType; -import io.rsocket.frame.PayloadFrameFlyweight; -import io.rsocket.frame.RequestChannelFrameFlyweight; -import io.rsocket.frame.RequestFireAndForgetFrameFlyweight; -import io.rsocket.frame.RequestResponseFrameFlyweight; -import io.rsocket.frame.RequestStreamFrameFlyweight; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; import java.util.function.Consumer; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -49,7 +49,7 @@ static Publisher fragmentFrame( boolean encodeLength) { ByteBuf metadata = getMetadata(frame, frameType); ByteBuf data = getData(frame, frameType); - int streamId = FrameHeaderFlyweight.streamId(frame); + int streamId = FrameHeaderCodec.streamId(frame); return Flux.generate( new Consumer>() { boolean first = true; @@ -84,7 +84,7 @@ static ByteBuf encodeFirstFragment( ByteBuf metadata, ByteBuf data) { // subtract the header bytes - int remaining = mtu - FrameHeaderFlyweight.size(); + int remaining = mtu - FrameHeaderCodec.size(); // substract the initial request n switch (frameType) { @@ -112,40 +112,40 @@ static ByteBuf encodeFirstFragment( switch (frameType) { case REQUEST_FNF: - return RequestFireAndForgetFrameFlyweight.encode( + return RequestFireAndForgetFrameCodec.encode( allocator, streamId, true, metadataFragment, dataFragment); case REQUEST_STREAM: - return RequestStreamFrameFlyweight.encode( + return RequestStreamFrameCodec.encode( allocator, streamId, true, - RequestStreamFrameFlyweight.initialRequestN(frame), + RequestStreamFrameCodec.initialRequestN(frame), metadataFragment, dataFragment); case REQUEST_RESPONSE: - return RequestResponseFrameFlyweight.encode( + return RequestResponseFrameCodec.encode( allocator, streamId, true, metadataFragment, dataFragment); case REQUEST_CHANNEL: - return RequestChannelFrameFlyweight.encode( + return RequestChannelFrameCodec.encode( allocator, streamId, true, false, - RequestChannelFrameFlyweight.initialRequestN(frame), + RequestChannelFrameCodec.initialRequestN(frame), metadataFragment, dataFragment); // Payload and synthetic types case PAYLOAD: - return PayloadFrameFlyweight.encode( + return PayloadFrameCodec.encode( allocator, streamId, true, false, false, metadataFragment, dataFragment); case NEXT: - return PayloadFrameFlyweight.encode( + return PayloadFrameCodec.encode( allocator, streamId, true, false, true, metadataFragment, dataFragment); case NEXT_COMPLETE: - return PayloadFrameFlyweight.encode( + return PayloadFrameCodec.encode( allocator, streamId, true, true, true, metadataFragment, dataFragment); case COMPLETE: - return PayloadFrameFlyweight.encode( + return PayloadFrameCodec.encode( allocator, streamId, true, true, false, metadataFragment, dataFragment); default: throw new IllegalStateException("unsupported fragment type: " + frameType); @@ -155,7 +155,7 @@ static ByteBuf encodeFirstFragment( static ByteBuf encodeFollowsFragment( ByteBufAllocator allocator, int mtu, int streamId, ByteBuf metadata, ByteBuf data) { // subtract the header bytes - int remaining = mtu - FrameHeaderFlyweight.size(); + int remaining = mtu - FrameHeaderCodec.size(); ByteBuf metadataFragment = null; if (metadata.isReadable()) { @@ -173,33 +173,33 @@ static ByteBuf encodeFollowsFragment( } boolean follows = data.isReadable() || metadata.isReadable(); - return PayloadFrameFlyweight.encode( + return PayloadFrameCodec.encode( allocator, streamId, follows, false, true, metadataFragment, dataFragment); } static ByteBuf getMetadata(ByteBuf frame, FrameType frameType) { - boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(frame); + boolean hasMetadata = FrameHeaderCodec.hasMetadata(frame); if (hasMetadata) { ByteBuf metadata; switch (frameType) { case REQUEST_FNF: - metadata = RequestFireAndForgetFrameFlyweight.metadata(frame); + metadata = RequestFireAndForgetFrameCodec.metadata(frame); break; case REQUEST_STREAM: - metadata = RequestStreamFrameFlyweight.metadata(frame); + metadata = RequestStreamFrameCodec.metadata(frame); break; case REQUEST_RESPONSE: - metadata = RequestResponseFrameFlyweight.metadata(frame); + metadata = RequestResponseFrameCodec.metadata(frame); break; case REQUEST_CHANNEL: - metadata = RequestChannelFrameFlyweight.metadata(frame); + metadata = RequestChannelFrameCodec.metadata(frame); break; // Payload and synthetic types case PAYLOAD: case NEXT: case NEXT_COMPLETE: case COMPLETE: - metadata = PayloadFrameFlyweight.metadata(frame); + metadata = PayloadFrameCodec.metadata(frame); break; default: throw new IllegalStateException("unsupported fragment type"); @@ -214,23 +214,23 @@ static ByteBuf getData(ByteBuf frame, FrameType frameType) { ByteBuf data; switch (frameType) { case REQUEST_FNF: - data = RequestFireAndForgetFrameFlyweight.data(frame); + data = RequestFireAndForgetFrameCodec.data(frame); break; case REQUEST_STREAM: - data = RequestStreamFrameFlyweight.data(frame); + data = RequestStreamFrameCodec.data(frame); break; case REQUEST_RESPONSE: - data = RequestResponseFrameFlyweight.data(frame); + data = RequestResponseFrameCodec.data(frame); break; case REQUEST_CHANNEL: - data = RequestChannelFrameFlyweight.data(frame); + data = RequestChannelFrameCodec.data(frame); break; // Payload and synthetic types case PAYLOAD: case NEXT: case NEXT_COMPLETE: case COMPLETE: - data = PayloadFrameFlyweight.data(frame); + data = PayloadFrameCodec.data(frame); break; default: throw new IllegalStateException("unsupported fragment type"); @@ -240,7 +240,7 @@ static ByteBuf getData(ByteBuf frame, FrameType frameType) { static ByteBuf encode(ByteBufAllocator allocator, ByteBuf frame, boolean encodeLength) { if (encodeLength) { - return FrameLengthFlyweight.encode(allocator, frame.readableBytes(), frame); + return FrameLengthCodec.encode(allocator, frame.readableBytes(), frame); } else { return frame; } diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java index 1a8d242b2..1e96bd1fc 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java @@ -146,12 +146,12 @@ void cancelAssemble(int streamId) { void handleNoFollowsFlag(ByteBuf frame, SynchronousSink sink, int streamId) { ByteBuf header = removeHeader(streamId); if (header != null) { - if (FrameHeaderFlyweight.hasMetadata(header)) { + if (FrameHeaderCodec.hasMetadata(header)) { ByteBuf assembledFrame = assembleFrameWithMetadata(frame, streamId, header); sink.next(assembledFrame); } else { ByteBuf data = assembleData(frame, streamId); - ByteBuf assembledFrame = FragmentationFlyweight.encode(allocator, header, data); + ByteBuf assembledFrame = FragmentationCodec.encode(allocator, header, data); sink.next(assembledFrame); } frame.release(); @@ -163,36 +163,36 @@ void handleNoFollowsFlag(ByteBuf frame, SynchronousSink sink, int strea void handleFollowsFlag(ByteBuf frame, int streamId, FrameType frameType) { ByteBuf header = getHeader(streamId); if (header == null) { - header = frame.copy(frame.readerIndex(), FrameHeaderFlyweight.size()); + header = frame.copy(frame.readerIndex(), FrameHeaderCodec.size()); if (frameType == FrameType.REQUEST_CHANNEL || frameType == FrameType.REQUEST_STREAM) { - long i = RequestChannelFrameFlyweight.initialRequestN(frame); + long i = RequestChannelFrameCodec.initialRequestN(frame); header.writeInt(i > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) i); } putHeader(streamId, header); } - if (FrameHeaderFlyweight.hasMetadata(frame)) { + if (FrameHeaderCodec.hasMetadata(frame)) { CompositeByteBuf metadata = getMetadata(streamId); switch (frameType) { case REQUEST_FNF: - metadata.addComponents(true, RequestFireAndForgetFrameFlyweight.metadata(frame).retain()); + metadata.addComponents(true, RequestFireAndForgetFrameCodec.metadata(frame).retain()); break; case REQUEST_STREAM: - metadata.addComponents(true, RequestStreamFrameFlyweight.metadata(frame).retain()); + metadata.addComponents(true, RequestStreamFrameCodec.metadata(frame).retain()); break; case REQUEST_RESPONSE: - metadata.addComponents(true, RequestResponseFrameFlyweight.metadata(frame).retain()); + metadata.addComponents(true, RequestResponseFrameCodec.metadata(frame).retain()); break; case REQUEST_CHANNEL: - metadata.addComponents(true, RequestChannelFrameFlyweight.metadata(frame).retain()); + metadata.addComponents(true, RequestChannelFrameCodec.metadata(frame).retain()); break; // Payload and synthetic types case PAYLOAD: case NEXT: case NEXT_COMPLETE: case COMPLETE: - metadata.addComponents(true, PayloadFrameFlyweight.metadata(frame).retain()); + metadata.addComponents(true, PayloadFrameCodec.metadata(frame).retain()); break; default: throw new IllegalStateException("unsupported fragment type"); @@ -202,23 +202,23 @@ void handleFollowsFlag(ByteBuf frame, int streamId, FrameType frameType) { ByteBuf data; switch (frameType) { case REQUEST_FNF: - data = RequestFireAndForgetFrameFlyweight.data(frame).retain(); + data = RequestFireAndForgetFrameCodec.data(frame).retain(); break; case REQUEST_STREAM: - data = RequestStreamFrameFlyweight.data(frame).retain(); + data = RequestStreamFrameCodec.data(frame).retain(); break; case REQUEST_RESPONSE: - data = RequestResponseFrameFlyweight.data(frame).retain(); + data = RequestResponseFrameCodec.data(frame).retain(); break; case REQUEST_CHANNEL: - data = RequestChannelFrameFlyweight.data(frame).retain(); + data = RequestChannelFrameCodec.data(frame).retain(); break; // Payload and synthetic types case PAYLOAD: case NEXT: case NEXT_COMPLETE: case COMPLETE: - data = PayloadFrameFlyweight.data(frame).retain(); + data = PayloadFrameCodec.data(frame).retain(); break; default: throw new IllegalStateException("unsupported fragment type"); @@ -230,8 +230,8 @@ void handleFollowsFlag(ByteBuf frame, int streamId, FrameType frameType) { void reassembleFrame(ByteBuf frame, SynchronousSink sink) { try { - FrameType frameType = FrameHeaderFlyweight.frameType(frame); - int streamId = FrameHeaderFlyweight.streamId(frame); + FrameType frameType = FrameHeaderCodec.frameType(frame); + int streamId = FrameHeaderCodec.streamId(frame); switch (frameType) { case CANCEL: case ERROR: @@ -244,7 +244,7 @@ void reassembleFrame(ByteBuf frame, SynchronousSink sink) { return; } - boolean hasFollows = FrameHeaderFlyweight.hasFollows(frame); + boolean hasFollows = FrameHeaderCodec.hasFollows(frame); if (hasFollows) { handleFollowsFlag(frame, streamId, frameType); @@ -262,12 +262,12 @@ private ByteBuf assembleFrameWithMetadata(ByteBuf frame, int streamId, ByteBuf h ByteBuf metadata; CompositeByteBuf cm = removeMetadata(streamId); - ByteBuf decodedMetadata = PayloadFrameFlyweight.metadata(frame); + ByteBuf decodedMetadata = PayloadFrameCodec.metadata(frame); if (decodedMetadata != null) { if (cm != null) { metadata = cm.addComponents(true, decodedMetadata.retain()); } else { - metadata = PayloadFrameFlyweight.metadata(frame).retain(); + metadata = PayloadFrameCodec.metadata(frame).retain(); } } else { metadata = cm != null ? cm : null; @@ -275,14 +275,14 @@ private ByteBuf assembleFrameWithMetadata(ByteBuf frame, int streamId, ByteBuf h ByteBuf data = assembleData(frame, streamId); - return FragmentationFlyweight.encode(allocator, header, metadata, data); + return FragmentationCodec.encode(allocator, header, metadata, data); } private ByteBuf assembleData(ByteBuf frame, int streamId) { ByteBuf data; CompositeByteBuf cd = removeData(streamId); if (cd != null) { - cd.addComponents(true, PayloadFrameFlyweight.data(frame).retain()); + cd.addComponents(true, PayloadFrameCodec.data(frame).retain()); data = cd; } else { data = Unpooled.EMPTY_BUFFER; diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java index 933755bb2..6060c0c20 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/ReassemblyDuplexConnection.java @@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; -import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.frame.FrameLengthCodec; import java.util.Objects; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -58,7 +58,7 @@ public Mono sendOne(ByteBuf frame) { private ByteBuf decode(ByteBuf frame) { if (decodeLength) { - return FrameLengthFlyweight.frame(frame).retain(); + return FrameLengthCodec.frame(frame).retain(); } else { return frame; } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/CancelFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/CancelFrameCodec.java similarity index 55% rename from rsocket-core/src/main/java/io/rsocket/frame/CancelFrameFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/CancelFrameCodec.java index 349a43c3a..d0d929f0f 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/CancelFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/CancelFrameCodec.java @@ -3,10 +3,10 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -public class CancelFrameFlyweight { - private CancelFrameFlyweight() {} +public class CancelFrameCodec { + private CancelFrameCodec() {} public static ByteBuf encode(final ByteBufAllocator allocator, final int streamId) { - return FrameHeaderFlyweight.encode(allocator, streamId, FrameType.CANCEL, 0); + return FrameHeaderCodec.encode(allocator, streamId, FrameType.CANCEL, 0); } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameCodec.java similarity index 89% rename from rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameCodec.java index ab26233f1..dcacb57dc 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/ErrorFrameCodec.java @@ -6,7 +6,7 @@ import io.rsocket.RSocketErrorException; import java.nio.charset.StandardCharsets; -public class ErrorFrameFlyweight { +public class ErrorFrameCodec { // defined zero stream id error codes public static final int INVALID_SETUP = 0x00000001; @@ -26,7 +26,7 @@ public class ErrorFrameFlyweight { public static ByteBuf encode( ByteBufAllocator allocator, int streamId, Throwable t, ByteBuf data) { - ByteBuf header = FrameHeaderFlyweight.encode(allocator, streamId, FrameType.ERROR, 0); + ByteBuf header = FrameHeaderCodec.encode(allocator, streamId, FrameType.ERROR, 0); int errorCode = t instanceof RSocketErrorException @@ -46,7 +46,7 @@ public static ByteBuf encode(ByteBufAllocator allocator, int streamId, Throwable public static int errorCode(ByteBuf byteBuf) { byteBuf.markReaderIndex(); - byteBuf.skipBytes(FrameHeaderFlyweight.size()); + byteBuf.skipBytes(FrameHeaderCodec.size()); int i = byteBuf.readInt(); byteBuf.resetReaderIndex(); return i; @@ -54,7 +54,7 @@ public static int errorCode(ByteBuf byteBuf) { public static ByteBuf data(ByteBuf byteBuf) { byteBuf.markReaderIndex(); - byteBuf.skipBytes(FrameHeaderFlyweight.size() + Integer.BYTES); + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES); ByteBuf slice = byteBuf.slice(); byteBuf.resetReaderIndex(); return slice; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ErrorType.java b/rsocket-core/src/main/java/io/rsocket/frame/ErrorType.java deleted file mode 100644 index b41a5d59e..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/ErrorType.java +++ /dev/null @@ -1,85 +0,0 @@ -package io.rsocket.frame; - -/** - * The types of {@link Error} that can be set. - * - * @see Error - * Codes - * @deprecated please use constants in {@link ErrorFrameFlyweight}. - */ -@Deprecated -public final class ErrorType { - - /** - * Application layer logic generating a Reactive Streams onError event. Stream ID MUST be > 0. - */ - public static final int APPLICATION_ERROR = 0x00000201; - - /** - * The Responder canceled the request but may have started processing it (similar to REJECTED but - * doesn't guarantee lack of side-effects). Stream ID MUST be > 0. - */ - public static final int CANCELED = 0x00000203; - - /** - * The connection is being terminated. Stream ID MUST be 0. Sender or Receiver of this frame MUST - * wait for outstanding streams to terminate before closing the connection. New requests MAY not - * be accepted. - */ - public static final int CONNECTION_CLOSE = 0x00000102; - - /** - * The connection is being terminated. Stream ID MUST be 0. Sender or Receiver of this frame MAY - * close the connection immediately without waiting for outstanding streams to terminate. - */ - public static final int CONNECTION_ERROR = 0x00000101; - - /** The request is invalid. Stream ID MUST be > 0. */ - public static final int INVALID = 0x00000204; - - /** - * The Setup frame is invalid for the server (it could be that the client is too recent for the - * old server). Stream ID MUST be 0. - */ - public static final int INVALID_SETUP = 0x00000001; - - /** - * Despite being a valid request, the Responder decided to reject it. The Responder guarantees - * that it didn't process the request. The reason for the rejection is explained in the Error Data - * section. Stream ID MUST be > 0. - */ - public static final int REJECTED = 0x00000202; - - /** - * The server rejected the resume, it can specify the reason in the payload. Stream ID MUST be 0. - */ - public static final int REJECTED_RESUME = 0x00000004; - - /** - * The server rejected the setup, it can specify the reason in the payload. Stream ID MUST be 0. - */ - public static final int REJECTED_SETUP = 0x00000003; - - /** Reserved. */ - public static final int RESERVED = 0x00000000; - - /** Reserved for Extension Use. */ - public static final int RESERVED_FOR_EXTENSION = 0xFFFFFFFF; - - /** - * Some (or all) of the parameters specified by the client are unsupported by the server. Stream - * ID MUST be 0. - */ - public static final int UNSUPPORTED_SETUP = 0x00000002; - - /** Minimum allowed user defined error code value */ - public static final int MIN_USER_ALLOWED_ERROR_CODE = 0x00000301; - - /** - * Maximum allowed user defined error code value. Note, the value is above signed integer maximum, - * so it will be negative after overflow. - */ - public static final int MAX_USER_ALLOWED_ERROR_CODE = 0xFFFFFFFE; - - private ErrorType() {} -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameCodec.java new file mode 100644 index 000000000..bf30b9556 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameCodec.java @@ -0,0 +1,66 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import javax.annotation.Nullable; + +public class ExtensionFrameCodec { + private ExtensionFrameCodec() {} + + public static ByteBuf encode( + ByteBufAllocator allocator, + int streamId, + int extendedType, + @Nullable ByteBuf metadata, + ByteBuf data) { + + final boolean hasMetadata = metadata != null; + + int flags = FrameHeaderCodec.FLAGS_I; + + if (hasMetadata) { + flags |= FrameHeaderCodec.FLAGS_M; + } + + final ByteBuf header = FrameHeaderCodec.encode(allocator, streamId, FrameType.EXT, flags); + header.writeInt(extendedType); + + return FrameBodyCodec.encode(allocator, header, metadata, hasMetadata, data); + } + + public static int extendedType(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.EXT, byteBuf); + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size()); + int i = byteBuf.readInt(); + byteBuf.resetReaderIndex(); + return i; + } + + public static ByteBuf data(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.EXT, byteBuf); + + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + byteBuf.markReaderIndex(); + // Extended type + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES); + ByteBuf data = FrameBodyCodec.dataWithoutMarking(byteBuf, hasMetadata); + byteBuf.resetReaderIndex(); + return data; + } + + public static ByteBuf metadata(ByteBuf byteBuf) { + FrameHeaderCodec.ensureFrameType(FrameType.EXT, byteBuf); + + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + if (!hasMetadata) { + return null; + } + byteBuf.markReaderIndex(); + // Extended type + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES); + ByteBuf metadata = FrameBodyCodec.metadataWithoutMarking(byteBuf); + byteBuf.resetReaderIndex(); + return metadata; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameFlyweight.java deleted file mode 100644 index 8cb01b08f..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameFlyweight.java +++ /dev/null @@ -1,66 +0,0 @@ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import javax.annotation.Nullable; - -public class ExtensionFrameFlyweight { - private ExtensionFrameFlyweight() {} - - public static ByteBuf encode( - ByteBufAllocator allocator, - int streamId, - int extendedType, - @Nullable ByteBuf metadata, - ByteBuf data) { - - final boolean hasMetadata = metadata != null; - - int flags = FrameHeaderFlyweight.FLAGS_I; - - if (hasMetadata) { - flags |= FrameHeaderFlyweight.FLAGS_M; - } - - final ByteBuf header = FrameHeaderFlyweight.encode(allocator, streamId, FrameType.EXT, flags); - header.writeInt(extendedType); - - return DataAndMetadataFlyweight.encode(allocator, header, metadata, hasMetadata, data); - } - - public static int extendedType(ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.EXT, byteBuf); - byteBuf.markReaderIndex(); - byteBuf.skipBytes(FrameHeaderFlyweight.size()); - int i = byteBuf.readInt(); - byteBuf.resetReaderIndex(); - return i; - } - - public static ByteBuf data(ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.EXT, byteBuf); - - boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(byteBuf); - byteBuf.markReaderIndex(); - // Extended type - byteBuf.skipBytes(FrameHeaderFlyweight.size() + Integer.BYTES); - ByteBuf data = DataAndMetadataFlyweight.dataWithoutMarking(byteBuf, hasMetadata); - byteBuf.resetReaderIndex(); - return data; - } - - public static ByteBuf metadata(ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.EXT, byteBuf); - - boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(byteBuf); - if (!hasMetadata) { - return null; - } - byteBuf.markReaderIndex(); - // Extended type - byteBuf.skipBytes(FrameHeaderFlyweight.size() + Integer.BYTES); - ByteBuf metadata = DataAndMetadataFlyweight.metadataWithoutMarking(byteBuf); - byteBuf.resetReaderIndex(); - return metadata; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FragmentationCodec.java similarity index 80% rename from rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/FragmentationCodec.java index a91d52782..de228b271 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FragmentationFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FragmentationCodec.java @@ -5,7 +5,7 @@ import reactor.util.annotation.Nullable; /** FragmentationFlyweight is used to re-assemble frames */ -public class FragmentationFlyweight { +public class FragmentationCodec { public static ByteBuf encode(final ByteBufAllocator allocator, ByteBuf header, ByteBuf data) { return encode(allocator, header, null, data); } @@ -14,6 +14,6 @@ public static ByteBuf encode( final ByteBufAllocator allocator, ByteBuf header, @Nullable ByteBuf metadata, ByteBuf data) { final boolean hasMetadata = metadata != null; - return DataAndMetadataFlyweight.encode(allocator, header, metadata, hasMetadata, data); + return FrameBodyCodec.encode(allocator, header, metadata, hasMetadata, data); } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameBodyCodec.java similarity index 97% rename from rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/FrameBodyCodec.java index 73bfd38f1..3256d4426 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/DataAndMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameBodyCodec.java @@ -4,10 +4,10 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; -class DataAndMetadataFlyweight { +class FrameBodyCodec { public static final int FRAME_LENGTH_MASK = 0xFFFFFF; - private DataAndMetadataFlyweight() {} + private FrameBodyCodec() {} private static void encodeLength(final ByteBuf byteBuf, final int length) { if ((length & ~FRAME_LENGTH_MASK) != 0) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderCodec.java similarity index 98% rename from rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderCodec.java index cbc677444..28f39459d 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameHeaderCodec.java @@ -12,7 +12,7 @@ * *

    Not thread-safe. Assumed to be used single-threaded */ -public final class FrameHeaderFlyweight { +public final class FrameHeaderCodec { /** (I)gnore flag: a value of 0 indicates the protocol can't ignore this frame */ public static final int FLAGS_I = 0b10_0000_0000; /** (M)etadata flag: a value of 1 indicates the frame contains metadata */ @@ -38,7 +38,7 @@ public final class FrameHeaderFlyweight { disableFrameTypeCheck = Boolean.getBoolean(DISABLE_FRAME_TYPE_CHECK); } - private FrameHeaderFlyweight() {} + private FrameHeaderCodec() {} static ByteBuf encodeStreamZero( final ByteBufAllocator allocator, final FrameType frameType, int flags) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthCodec.java similarity index 95% rename from rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/FrameLengthCodec.java index 622160061..f6c19c8ee 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameLengthCodec.java @@ -7,11 +7,11 @@ * Some transports like TCP aren't framed, and require a length. This is used by DuplexConnections * for transports that need to send length */ -public class FrameLengthFlyweight { +public class FrameLengthCodec { public static final int FRAME_LENGTH_MASK = 0xFFFFFF; public static final int FRAME_LENGTH_SIZE = 3; - private FrameLengthFlyweight() {} + private FrameLengthCodec() {} private static void encodeLength(final ByteBuf byteBuf, final int length) { if ((length & ~FRAME_LENGTH_MASK) != 0) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java index 6662d34af..66d18c8a7 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameUtil.java @@ -9,8 +9,8 @@ public class FrameUtil { private FrameUtil() {} public static String toString(ByteBuf frame) { - FrameType frameType = FrameHeaderFlyweight.frameType(frame); - int streamId = FrameHeaderFlyweight.streamId(frame); + FrameType frameType = FrameHeaderCodec.frameType(frame); + int streamId = FrameHeaderCodec.streamId(frame); StringBuilder payload = new StringBuilder(); payload @@ -19,20 +19,18 @@ public static String toString(ByteBuf frame) { .append(" Type: ") .append(frameType) .append(" Flags: 0b") - .append(Integer.toBinaryString(FrameHeaderFlyweight.flags(frame))) + .append(Integer.toBinaryString(FrameHeaderCodec.flags(frame))) .append(" Length: " + frame.readableBytes()); if (frameType.hasInitialRequestN()) { - payload - .append(" InitialRequestN: ") - .append(RequestStreamFrameFlyweight.initialRequestN(frame)); + payload.append(" InitialRequestN: ").append(RequestStreamFrameCodec.initialRequestN(frame)); } if (frameType == FrameType.REQUEST_N) { - payload.append(" RequestN: ").append(RequestNFrameFlyweight.requestN(frame)); + payload.append(" RequestN: ").append(RequestNFrameCodec.requestN(frame)); } - if (FrameHeaderFlyweight.hasMetadata(frame)) { + if (FrameHeaderCodec.hasMetadata(frame)) { payload.append("\nMetadata:\n"); ByteBufUtil.appendPrettyHexDump(payload, getMetadata(frame, frameType)); @@ -45,37 +43,37 @@ public static String toString(ByteBuf frame) { } private static ByteBuf getMetadata(ByteBuf frame, FrameType frameType) { - boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(frame); + boolean hasMetadata = FrameHeaderCodec.hasMetadata(frame); if (hasMetadata) { ByteBuf metadata; switch (frameType) { case REQUEST_FNF: - metadata = RequestFireAndForgetFrameFlyweight.metadata(frame); + metadata = RequestFireAndForgetFrameCodec.metadata(frame); break; case REQUEST_STREAM: - metadata = RequestStreamFrameFlyweight.metadata(frame); + metadata = RequestStreamFrameCodec.metadata(frame); break; case REQUEST_RESPONSE: - metadata = RequestResponseFrameFlyweight.metadata(frame); + metadata = RequestResponseFrameCodec.metadata(frame); break; case REQUEST_CHANNEL: - metadata = RequestChannelFrameFlyweight.metadata(frame); + metadata = RequestChannelFrameCodec.metadata(frame); break; // Payload and synthetic types case PAYLOAD: case NEXT: case NEXT_COMPLETE: case COMPLETE: - metadata = PayloadFrameFlyweight.metadata(frame); + metadata = PayloadFrameCodec.metadata(frame); break; case METADATA_PUSH: - metadata = MetadataPushFrameFlyweight.metadata(frame); + metadata = MetadataPushFrameCodec.metadata(frame); break; case SETUP: - metadata = SetupFrameFlyweight.metadata(frame); + metadata = SetupFrameCodec.metadata(frame); break; case LEASE: - metadata = LeaseFrameFlyweight.metadata(frame); + metadata = LeaseFrameCodec.metadata(frame); break; default: return Unpooled.EMPTY_BUFFER; @@ -90,26 +88,26 @@ private static ByteBuf getData(ByteBuf frame, FrameType frameType) { ByteBuf data; switch (frameType) { case REQUEST_FNF: - data = RequestFireAndForgetFrameFlyweight.data(frame); + data = RequestFireAndForgetFrameCodec.data(frame); break; case REQUEST_STREAM: - data = RequestStreamFrameFlyweight.data(frame); + data = RequestStreamFrameCodec.data(frame); break; case REQUEST_RESPONSE: - data = RequestResponseFrameFlyweight.data(frame); + data = RequestResponseFrameCodec.data(frame); break; case REQUEST_CHANNEL: - data = RequestChannelFrameFlyweight.data(frame); + data = RequestChannelFrameCodec.data(frame); break; // Payload and synthetic types case PAYLOAD: case NEXT: case NEXT_COMPLETE: case COMPLETE: - data = PayloadFrameFlyweight.data(frame); + data = PayloadFrameCodec.data(frame); break; case SETUP: - data = SetupFrameFlyweight.data(frame); + data = SetupFrameCodec.data(frame); break; default: return Unpooled.EMPTY_BUFFER; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/GenericFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/GenericFrameCodec.java new file mode 100644 index 000000000..65e7eeeea --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/GenericFrameCodec.java @@ -0,0 +1,157 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.IllegalReferenceCountException; +import io.rsocket.Payload; +import javax.annotation.Nullable; + +class GenericFrameCodec { + + static ByteBuf encodeReleasingPayload( + final ByteBufAllocator allocator, + final FrameType frameType, + final int streamId, + boolean complete, + boolean next, + final Payload payload) { + return encodeReleasingPayload(allocator, frameType, streamId, complete, next, 0, payload); + } + + static ByteBuf encodeReleasingPayload( + final ByteBufAllocator allocator, + final FrameType frameType, + final int streamId, + boolean complete, + boolean next, + int requestN, + final Payload payload) { + + // if refCnt exceptions throws here it is safe to do no-op + boolean hasMetadata = payload.hasMetadata(); + // if refCnt exceptions throws here it is safe to do no-op still + final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; + final ByteBuf data; + // retaining data safely. May throw either NPE or RefCntE + try { + data = payload.data().retain(); + } catch (IllegalReferenceCountException | NullPointerException e) { + if (hasMetadata) { + metadata.release(); + } + throw e; + } + // releasing payload safely since it can be already released wheres we have to release retained + // data and metadata as well + try { + payload.release(); + } catch (IllegalReferenceCountException e) { + data.release(); + if (hasMetadata) { + metadata.release(); + } + throw e; + } + + return encode(allocator, frameType, streamId, false, complete, next, requestN, metadata, data); + } + + static ByteBuf encode( + final ByteBufAllocator allocator, + final FrameType frameType, + final int streamId, + boolean fragmentFollows, + @Nullable ByteBuf metadata, + ByteBuf data) { + return encode(allocator, frameType, streamId, fragmentFollows, false, false, 0, metadata, data); + } + + static ByteBuf encode( + final ByteBufAllocator allocator, + final FrameType frameType, + final int streamId, + boolean fragmentFollows, + boolean complete, + boolean next, + int requestN, + @Nullable ByteBuf metadata, + ByteBuf data) { + + final boolean hasMetadata = metadata != null; + + int flags = 0; + + if (hasMetadata) { + flags |= FrameHeaderCodec.FLAGS_M; + } + + if (fragmentFollows) { + flags |= FrameHeaderCodec.FLAGS_F; + } + + if (complete) { + flags |= FrameHeaderCodec.FLAGS_C; + } + + if (next) { + flags |= FrameHeaderCodec.FLAGS_N; + } + + final ByteBuf header = FrameHeaderCodec.encode(allocator, streamId, frameType, flags); + + if (requestN > 0) { + header.writeInt(requestN); + } + + return FrameBodyCodec.encode(allocator, header, metadata, hasMetadata, data); + } + + static ByteBuf data(ByteBuf byteBuf) { + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + int idx = byteBuf.readerIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size()); + ByteBuf data = FrameBodyCodec.dataWithoutMarking(byteBuf, hasMetadata); + byteBuf.readerIndex(idx); + return data; + } + + static ByteBuf metadata(ByteBuf byteBuf) { + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + if (!hasMetadata) { + return null; + } + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size()); + ByteBuf metadata = FrameBodyCodec.metadataWithoutMarking(byteBuf); + byteBuf.resetReaderIndex(); + return metadata; + } + + static ByteBuf dataWithRequestN(ByteBuf byteBuf) { + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES); + ByteBuf data = FrameBodyCodec.dataWithoutMarking(byteBuf, hasMetadata); + byteBuf.resetReaderIndex(); + return data; + } + + static ByteBuf metadataWithRequestN(ByteBuf byteBuf) { + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); + if (!hasMetadata) { + return null; + } + byteBuf.markReaderIndex(); + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES); + ByteBuf metadata = FrameBodyCodec.metadataWithoutMarking(byteBuf); + byteBuf.resetReaderIndex(); + return metadata; + } + + static int initialRequestN(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + int i = byteBuf.skipBytes(FrameHeaderCodec.size()).readInt(); + byteBuf.resetReaderIndex(); + return i; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/KeepAliveFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/KeepAliveFrameCodec.java similarity index 61% rename from rsocket-core/src/main/java/io/rsocket/frame/KeepAliveFrameFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/KeepAliveFrameCodec.java index b591412a6..752d5b3eb 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/KeepAliveFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/KeepAliveFrameCodec.java @@ -3,7 +3,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -public class KeepAliveFrameFlyweight { +public class KeepAliveFrameCodec { /** * (R)espond: Set by the sender of the KEEPALIVE, to which the responder MUST reply with a * KEEPALIVE without the R flag set @@ -12,7 +12,7 @@ public class KeepAliveFrameFlyweight { public static final long LAST_POSITION_MASK = 0x8000000000000000L; - private KeepAliveFrameFlyweight() {} + private KeepAliveFrameCodec() {} public static ByteBuf encode( final ByteBufAllocator allocator, @@ -20,7 +20,7 @@ public static ByteBuf encode( final long lastPosition, final ByteBuf data) { final int flags = respond ? FLAGS_KEEPALIVE_R : 0; - ByteBuf header = FrameHeaderFlyweight.encodeStreamZero(allocator, FrameType.KEEPALIVE, flags); + ByteBuf header = FrameHeaderCodec.encodeStreamZero(allocator, FrameType.KEEPALIVE, flags); long lp = 0; if (lastPosition > 0) { @@ -29,27 +29,27 @@ public static ByteBuf encode( header.writeLong(lp); - return DataAndMetadataFlyweight.encode(allocator, header, null, false, data); + return FrameBodyCodec.encode(allocator, header, null, false, data); } public static boolean respondFlag(ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.KEEPALIVE, byteBuf); - int flags = FrameHeaderFlyweight.flags(byteBuf); + FrameHeaderCodec.ensureFrameType(FrameType.KEEPALIVE, byteBuf); + int flags = FrameHeaderCodec.flags(byteBuf); return (flags & FLAGS_KEEPALIVE_R) == FLAGS_KEEPALIVE_R; } public static long lastPosition(ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.KEEPALIVE, byteBuf); + FrameHeaderCodec.ensureFrameType(FrameType.KEEPALIVE, byteBuf); byteBuf.markReaderIndex(); - long l = byteBuf.skipBytes(FrameHeaderFlyweight.size()).readLong(); + long l = byteBuf.skipBytes(FrameHeaderCodec.size()).readLong(); byteBuf.resetReaderIndex(); return l; } public static ByteBuf data(ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.KEEPALIVE, byteBuf); + FrameHeaderCodec.ensureFrameType(FrameType.KEEPALIVE, byteBuf); byteBuf.markReaderIndex(); - ByteBuf slice = byteBuf.skipBytes(FrameHeaderFlyweight.size() + Long.BYTES).slice(); + ByteBuf slice = byteBuf.skipBytes(FrameHeaderCodec.size() + Long.BYTES).slice(); byteBuf.resetReaderIndex(); return slice; } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameCodec.java similarity index 74% rename from rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameCodec.java index 32f086a15..cafd80104 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameCodec.java @@ -5,7 +5,7 @@ import io.netty.buffer.Unpooled; import javax.annotation.Nullable; -public class LeaseFrameFlyweight { +public class LeaseFrameCodec { public static ByteBuf encode( final ByteBufAllocator allocator, @@ -18,11 +18,11 @@ public static ByteBuf encode( int flags = 0; if (hasMetadata) { - flags |= FrameHeaderFlyweight.FLAGS_M; + flags |= FrameHeaderCodec.FLAGS_M; } final ByteBuf header = - FrameHeaderFlyweight.encodeStreamZero(allocator, FrameType.LEASE, flags) + FrameHeaderCodec.encodeStreamZero(allocator, FrameType.LEASE, flags) .writeInt(ttl) .writeInt(numRequests); @@ -49,30 +49,30 @@ public static ByteBuf encode( } public static int ttl(final ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.LEASE, byteBuf); + FrameHeaderCodec.ensureFrameType(FrameType.LEASE, byteBuf); byteBuf.markReaderIndex(); - byteBuf.skipBytes(FrameHeaderFlyweight.size()); + byteBuf.skipBytes(FrameHeaderCodec.size()); int ttl = byteBuf.readInt(); byteBuf.resetReaderIndex(); return ttl; } public static int numRequests(final ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.LEASE, byteBuf); + FrameHeaderCodec.ensureFrameType(FrameType.LEASE, byteBuf); byteBuf.markReaderIndex(); // Ttl - byteBuf.skipBytes(FrameHeaderFlyweight.size() + Integer.BYTES); + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES); int numRequests = byteBuf.readInt(); byteBuf.resetReaderIndex(); return numRequests; } public static ByteBuf metadata(final ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.LEASE, byteBuf); - if (FrameHeaderFlyweight.hasMetadata(byteBuf)) { + FrameHeaderCodec.ensureFrameType(FrameType.LEASE, byteBuf); + if (FrameHeaderCodec.hasMetadata(byteBuf)) { byteBuf.markReaderIndex(); // Ttl + Num of requests - byteBuf.skipBytes(FrameHeaderFlyweight.size() + Integer.BYTES * 2); + byteBuf.skipBytes(FrameHeaderCodec.size() + Integer.BYTES * 2); ByteBuf metadata = byteBuf.slice(); byteBuf.resetReaderIndex(); return metadata; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameCodec.java similarity index 83% rename from rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameCodec.java index a39acef92..62c8a17dc 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameCodec.java @@ -5,7 +5,7 @@ import io.netty.util.IllegalReferenceCountException; import io.rsocket.Payload; -public class MetadataPushFrameFlyweight { +public class MetadataPushFrameCodec { public static ByteBuf encodeReleasingPayload(ByteBufAllocator allocator, Payload payload) { final ByteBuf metadata = payload.metadata().retain(); @@ -22,14 +22,14 @@ public static ByteBuf encodeReleasingPayload(ByteBufAllocator allocator, Payload public static ByteBuf encode(ByteBufAllocator allocator, ByteBuf metadata) { ByteBuf header = - FrameHeaderFlyweight.encodeStreamZero( - allocator, FrameType.METADATA_PUSH, FrameHeaderFlyweight.FLAGS_M); + FrameHeaderCodec.encodeStreamZero( + allocator, FrameType.METADATA_PUSH, FrameHeaderCodec.FLAGS_M); return allocator.compositeBuffer(2).addComponents(true, header, metadata); } public static ByteBuf metadata(ByteBuf byteBuf) { byteBuf.markReaderIndex(); - int headerSize = FrameHeaderFlyweight.size(); + int headerSize = FrameHeaderCodec.size(); int metadataLength = byteBuf.readableBytes() - headerSize; byteBuf.skipBytes(headerSize); ByteBuf metadata = byteBuf.readSlice(metadataLength); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameCodec.java new file mode 100644 index 000000000..8a7e6427a --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameCodec.java @@ -0,0 +1,54 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; + +public class PayloadFrameCodec { + + private PayloadFrameCodec() {} + + public static ByteBuf encodeNextReleasingPayload( + ByteBufAllocator allocator, int streamId, Payload payload) { + + return encodeReleasingPayload(allocator, streamId, false, payload); + } + + public static ByteBuf encodeNextCompleteReleasingPayload( + ByteBufAllocator allocator, int streamId, Payload payload) { + + return encodeReleasingPayload(allocator, streamId, true, payload); + } + + static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, int streamId, boolean complete, Payload payload) { + + return GenericFrameCodec.encodeReleasingPayload( + allocator, FrameType.PAYLOAD, streamId, complete, true, payload); + } + + public static ByteBuf encodeComplete(ByteBufAllocator allocator, int streamId) { + return encode(allocator, streamId, false, true, false, null, null); + } + + public static ByteBuf encode( + ByteBufAllocator allocator, + int streamId, + boolean fragmentFollows, + boolean complete, + boolean next, + ByteBuf metadata, + ByteBuf data) { + + return GenericFrameCodec.encode( + allocator, FrameType.PAYLOAD, streamId, fragmentFollows, complete, next, 0, metadata, data); + } + + public static ByteBuf data(ByteBuf byteBuf) { + return GenericFrameCodec.data(byteBuf); + } + + public static ByteBuf metadata(ByteBuf byteBuf) { + return GenericFrameCodec.metadata(byteBuf); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java deleted file mode 100644 index 53ac6150b..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameFlyweight.java +++ /dev/null @@ -1,79 +0,0 @@ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.util.IllegalReferenceCountException; -import io.rsocket.Payload; - -public class PayloadFrameFlyweight { - private static final RequestFlyweight FLYWEIGHT = new RequestFlyweight(FrameType.PAYLOAD); - - private PayloadFrameFlyweight() {} - - public static ByteBuf encodeNextReleasingPayload( - ByteBufAllocator allocator, int streamId, Payload payload) { - return encodeReleasingPayload(allocator, streamId, false, payload); - } - - public static ByteBuf encodeNextCompleteReleasingPayload( - ByteBufAllocator allocator, int streamId, Payload payload) { - - return encodeReleasingPayload(allocator, streamId, true, payload); - } - - static ByteBuf encodeReleasingPayload( - ByteBufAllocator allocator, int streamId, boolean complete, Payload payload) { - - // if refCnt exceptions throws here it is safe to do no-op - boolean hasMetadata = payload.hasMetadata(); - // if refCnt exceptions throws here it is safe to do no-op still - final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; - final ByteBuf data; - // retaining data safely. May throw either NPE or RefCntE - try { - data = payload.data().retain(); - } catch (IllegalReferenceCountException | NullPointerException e) { - if (hasMetadata) { - metadata.release(); - } - throw e; - } - // releasing payload safely since it can be already released wheres we have to release retained - // data and metadata as well - try { - payload.release(); - } catch (IllegalReferenceCountException e) { - data.release(); - if (hasMetadata) { - metadata.release(); - } - throw e; - } - - return encode(allocator, streamId, false, complete, true, metadata, data); - } - - public static ByteBuf encodeComplete(ByteBufAllocator allocator, int streamId) { - return encode(allocator, streamId, false, true, false, null, null); - } - - public static ByteBuf encode( - ByteBufAllocator allocator, - int streamId, - boolean fragmentFollows, - boolean complete, - boolean next, - ByteBuf metadata, - ByteBuf data) { - return FLYWEIGHT.encode( - allocator, streamId, fragmentFollows, complete, next, 0, metadata, data); - } - - public static ByteBuf data(ByteBuf byteBuf) { - return FLYWEIGHT.data(byteBuf); - } - - public static ByteBuf metadata(ByteBuf byteBuf) { - return FLYWEIGHT.metadata(byteBuf); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameCodec.java new file mode 100644 index 000000000..2ff887043 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameCodec.java @@ -0,0 +1,67 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; + +public class RequestChannelFrameCodec { + + private RequestChannelFrameCodec() {} + + public static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, + int streamId, + boolean complete, + long initialRequestN, + Payload payload) { + + if (initialRequestN < 1) { + throw new IllegalArgumentException("request n is less than 1"); + } + + int reqN = initialRequestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) initialRequestN; + + return GenericFrameCodec.encodeReleasingPayload( + allocator, FrameType.REQUEST_CHANNEL, streamId, complete, false, reqN, payload); + } + + public static ByteBuf encode( + ByteBufAllocator allocator, + int streamId, + boolean fragmentFollows, + boolean complete, + long initialRequestN, + ByteBuf metadata, + ByteBuf data) { + + if (initialRequestN < 1) { + throw new IllegalArgumentException("request n is less than 1"); + } + + int reqN = initialRequestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) initialRequestN; + + return GenericFrameCodec.encode( + allocator, + FrameType.REQUEST_CHANNEL, + streamId, + fragmentFollows, + complete, + false, + reqN, + metadata, + data); + } + + public static ByteBuf data(ByteBuf byteBuf) { + return GenericFrameCodec.dataWithRequestN(byteBuf); + } + + public static ByteBuf metadata(ByteBuf byteBuf) { + return GenericFrameCodec.metadataWithRequestN(byteBuf); + } + + public static long initialRequestN(ByteBuf byteBuf) { + int requestN = GenericFrameCodec.initialRequestN(byteBuf); + return requestN == Integer.MAX_VALUE ? Long.MAX_VALUE : requestN; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java deleted file mode 100644 index c0db21170..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameFlyweight.java +++ /dev/null @@ -1,81 +0,0 @@ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.util.IllegalReferenceCountException; -import io.rsocket.Payload; - -public class RequestChannelFrameFlyweight { - - private static final RequestFlyweight FLYWEIGHT = new RequestFlyweight(FrameType.REQUEST_CHANNEL); - - private RequestChannelFrameFlyweight() {} - - public static ByteBuf encodeReleasingPayload( - ByteBufAllocator allocator, - int streamId, - boolean complete, - long initialRequestN, - Payload payload) { - - // if refCnt exceptions throws here it is safe to do no-op - boolean hasMetadata = payload.hasMetadata(); - // if refCnt exceptions throws here it is safe to do no-op still - final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; - final ByteBuf data; - // retaining data safely. May throw either NPE or RefCntE - try { - data = payload.data().retain(); - } catch (IllegalReferenceCountException | NullPointerException e) { - if (hasMetadata) { - metadata.release(); - } - throw e; - } - // releasing payload safely since it can be already released wheres we have to release retained - // data and metadata as well - try { - payload.release(); - } catch (IllegalReferenceCountException e) { - data.release(); - if (hasMetadata) { - metadata.release(); - } - throw e; - } - - return encode(allocator, streamId, false, complete, initialRequestN, metadata, data); - } - - public static ByteBuf encode( - ByteBufAllocator allocator, - int streamId, - boolean fragmentFollows, - boolean complete, - long initialRequestN, - ByteBuf metadata, - ByteBuf data) { - - if (initialRequestN < 1) { - throw new IllegalArgumentException("request n is less than 1"); - } - - int reqN = initialRequestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) initialRequestN; - - return FLYWEIGHT.encode( - allocator, streamId, fragmentFollows, complete, false, reqN, metadata, data); - } - - public static ByteBuf data(ByteBuf byteBuf) { - return FLYWEIGHT.dataWithRequestN(byteBuf); - } - - public static ByteBuf metadata(ByteBuf byteBuf) { - return FLYWEIGHT.metadataWithRequestN(byteBuf); - } - - public static long initialRequestN(ByteBuf byteBuf) { - int requestN = FLYWEIGHT.initialRequestN(byteBuf); - return requestN == Integer.MAX_VALUE ? Long.MAX_VALUE : requestN; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameCodec.java new file mode 100644 index 000000000..ddb5bc5d7 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameCodec.java @@ -0,0 +1,36 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; + +public class RequestFireAndForgetFrameCodec { + + private RequestFireAndForgetFrameCodec() {} + + public static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, int streamId, Payload payload) { + + return GenericFrameCodec.encodeReleasingPayload( + allocator, FrameType.REQUEST_FNF, streamId, false, false, payload); + } + + public static ByteBuf encode( + ByteBufAllocator allocator, + int streamId, + boolean fragmentFollows, + ByteBuf metadata, + ByteBuf data) { + + return GenericFrameCodec.encode( + allocator, FrameType.REQUEST_FNF, streamId, fragmentFollows, metadata, data); + } + + public static ByteBuf data(ByteBuf byteBuf) { + return GenericFrameCodec.data(byteBuf); + } + + public static ByteBuf metadata(ByteBuf byteBuf) { + return GenericFrameCodec.metadata(byteBuf); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java deleted file mode 100644 index e091edcc3..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameFlyweight.java +++ /dev/null @@ -1,63 +0,0 @@ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.util.IllegalReferenceCountException; -import io.rsocket.Payload; - -public class RequestFireAndForgetFrameFlyweight { - - private static final RequestFlyweight FLYWEIGHT = new RequestFlyweight(FrameType.REQUEST_FNF); - - private RequestFireAndForgetFrameFlyweight() {} - - public static ByteBuf encodeReleasingPayload( - ByteBufAllocator allocator, int streamId, Payload payload) { - - // if refCnt exceptions throws here it is safe to do no-op - boolean hasMetadata = payload.hasMetadata(); - // if refCnt exceptions throws here it is safe to do no-op still - final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; - final ByteBuf data; - // retaining data safely. May throw either NPE or RefCntE - try { - data = payload.data().retain(); - } catch (IllegalReferenceCountException | NullPointerException e) { - if (hasMetadata) { - metadata.release(); - } - throw e; - } - // releasing payload safely since it can be already released wheres we have to release retained - // data and metadata as well - try { - payload.release(); - } catch (IllegalReferenceCountException e) { - data.release(); - if (hasMetadata) { - metadata.release(); - } - throw e; - } - - return FLYWEIGHT.encode(allocator, streamId, false, metadata, data); - } - - public static ByteBuf encode( - ByteBufAllocator allocator, - int streamId, - boolean fragmentFollows, - ByteBuf metadata, - ByteBuf data) { - - return FLYWEIGHT.encode(allocator, streamId, fragmentFollows, metadata, data); - } - - public static ByteBuf data(ByteBuf byteBuf) { - return FLYWEIGHT.data(byteBuf); - } - - public static ByteBuf metadata(ByteBuf byteBuf) { - return FLYWEIGHT.metadata(byteBuf); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestFlyweight.java deleted file mode 100644 index 15fac9f55..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestFlyweight.java +++ /dev/null @@ -1,110 +0,0 @@ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import javax.annotation.Nullable; - -class RequestFlyweight { - FrameType frameType; - - RequestFlyweight(FrameType frameType) { - this.frameType = frameType; - } - - ByteBuf encode( - final ByteBufAllocator allocator, - final int streamId, - boolean fragmentFollows, - @Nullable ByteBuf metadata, - ByteBuf data) { - return encode(allocator, streamId, fragmentFollows, false, false, 0, metadata, data); - } - - ByteBuf encode( - final ByteBufAllocator allocator, - final int streamId, - boolean fragmentFollows, - boolean complete, - boolean next, - int requestN, - @Nullable ByteBuf metadata, - ByteBuf data) { - - final boolean hasMetadata = metadata != null; - - int flags = 0; - - if (hasMetadata) { - flags |= FrameHeaderFlyweight.FLAGS_M; - } - - if (fragmentFollows) { - flags |= FrameHeaderFlyweight.FLAGS_F; - } - - if (complete) { - flags |= FrameHeaderFlyweight.FLAGS_C; - } - - if (next) { - flags |= FrameHeaderFlyweight.FLAGS_N; - } - - final ByteBuf header = FrameHeaderFlyweight.encode(allocator, streamId, frameType, flags); - - if (requestN > 0) { - header.writeInt(requestN); - } - - return DataAndMetadataFlyweight.encode(allocator, header, metadata, hasMetadata, data); - } - - ByteBuf data(ByteBuf byteBuf) { - boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(byteBuf); - int idx = byteBuf.readerIndex(); - byteBuf.skipBytes(FrameHeaderFlyweight.size()); - ByteBuf data = DataAndMetadataFlyweight.dataWithoutMarking(byteBuf, hasMetadata); - byteBuf.readerIndex(idx); - return data; - } - - ByteBuf metadata(ByteBuf byteBuf) { - boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(byteBuf); - if (!hasMetadata) { - return null; - } - byteBuf.markReaderIndex(); - byteBuf.skipBytes(FrameHeaderFlyweight.size()); - ByteBuf metadata = DataAndMetadataFlyweight.metadataWithoutMarking(byteBuf); - byteBuf.resetReaderIndex(); - return metadata; - } - - ByteBuf dataWithRequestN(ByteBuf byteBuf) { - boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(byteBuf); - byteBuf.markReaderIndex(); - byteBuf.skipBytes(FrameHeaderFlyweight.size() + Integer.BYTES); - ByteBuf data = DataAndMetadataFlyweight.dataWithoutMarking(byteBuf, hasMetadata); - byteBuf.resetReaderIndex(); - return data; - } - - ByteBuf metadataWithRequestN(ByteBuf byteBuf) { - boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(byteBuf); - if (!hasMetadata) { - return null; - } - byteBuf.markReaderIndex(); - byteBuf.skipBytes(FrameHeaderFlyweight.size() + Integer.BYTES); - ByteBuf metadata = DataAndMetadataFlyweight.metadataWithoutMarking(byteBuf); - byteBuf.resetReaderIndex(); - return metadata; - } - - int initialRequestN(ByteBuf byteBuf) { - byteBuf.markReaderIndex(); - int i = byteBuf.skipBytes(FrameHeaderFlyweight.size()).readInt(); - byteBuf.resetReaderIndex(); - return i; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameCodec.java similarity index 68% rename from rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameCodec.java index fe2c752cf..66bdd46f4 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestNFrameCodec.java @@ -3,8 +3,8 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -public class RequestNFrameFlyweight { - private RequestNFrameFlyweight() {} +public class RequestNFrameCodec { + private RequestNFrameCodec() {} public static ByteBuf encode( final ByteBufAllocator allocator, final int streamId, long requestN) { @@ -15,14 +15,14 @@ public static ByteBuf encode( int reqN = requestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) requestN; - ByteBuf header = FrameHeaderFlyweight.encode(allocator, streamId, FrameType.REQUEST_N, 0); + ByteBuf header = FrameHeaderCodec.encode(allocator, streamId, FrameType.REQUEST_N, 0); return header.writeInt(reqN); } public static long requestN(ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.REQUEST_N, byteBuf); + FrameHeaderCodec.ensureFrameType(FrameType.REQUEST_N, byteBuf); byteBuf.markReaderIndex(); - byteBuf.skipBytes(FrameHeaderFlyweight.size()); + byteBuf.skipBytes(FrameHeaderCodec.size()); int i = byteBuf.readInt(); byteBuf.resetReaderIndex(); return i == Integer.MAX_VALUE ? Long.MAX_VALUE : i; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameCodec.java new file mode 100644 index 000000000..884a8293b --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameCodec.java @@ -0,0 +1,35 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; + +public class RequestResponseFrameCodec { + + private RequestResponseFrameCodec() {} + + public static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, int streamId, Payload payload) { + + return GenericFrameCodec.encodeReleasingPayload( + allocator, FrameType.REQUEST_RESPONSE, streamId, false, false, payload); + } + + public static ByteBuf encode( + ByteBufAllocator allocator, + int streamId, + boolean fragmentFollows, + ByteBuf metadata, + ByteBuf data) { + return GenericFrameCodec.encode( + allocator, FrameType.REQUEST_RESPONSE, streamId, fragmentFollows, metadata, data); + } + + public static ByteBuf data(ByteBuf byteBuf) { + return GenericFrameCodec.data(byteBuf); + } + + public static ByteBuf metadata(ByteBuf byteBuf) { + return GenericFrameCodec.metadata(byteBuf); + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java deleted file mode 100644 index 782c70965..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameFlyweight.java +++ /dev/null @@ -1,62 +0,0 @@ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.util.IllegalReferenceCountException; -import io.rsocket.Payload; - -public class RequestResponseFrameFlyweight { - private static final RequestFlyweight FLYWEIGHT = - new RequestFlyweight(FrameType.REQUEST_RESPONSE); - - private RequestResponseFrameFlyweight() {} - - public static ByteBuf encodeReleasingPayload( - ByteBufAllocator allocator, int streamId, Payload payload) { - - // if refCnt exceptions throws here it is safe to do no-op - boolean hasMetadata = payload.hasMetadata(); - // if refCnt exceptions throws here it is safe to do no-op still - final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; - final ByteBuf data; - // retaining data safely. May throw either NPE or RefCntE - try { - data = payload.data().retain(); - } catch (IllegalReferenceCountException | NullPointerException e) { - if (hasMetadata) { - metadata.release(); - } - throw e; - } - // releasing payload safely since it can be already released wheres we have to release retained - // data and metadata as well - try { - payload.release(); - } catch (IllegalReferenceCountException e) { - data.release(); - if (hasMetadata) { - metadata.release(); - } - throw e; - } - - return encode(allocator, streamId, false, metadata, data); - } - - public static ByteBuf encode( - ByteBufAllocator allocator, - int streamId, - boolean fragmentFollows, - ByteBuf metadata, - ByteBuf data) { - return FLYWEIGHT.encode(allocator, streamId, fragmentFollows, metadata, data); - } - - public static ByteBuf data(ByteBuf byteBuf) { - return FLYWEIGHT.data(byteBuf); - } - - public static ByteBuf metadata(ByteBuf byteBuf) { - return FLYWEIGHT.metadata(byteBuf); - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameCodec.java new file mode 100644 index 000000000..9853a8b54 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameCodec.java @@ -0,0 +1,62 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.rsocket.Payload; + +public class RequestStreamFrameCodec { + + private RequestStreamFrameCodec() {} + + public static ByteBuf encodeReleasingPayload( + ByteBufAllocator allocator, int streamId, long initialRequestN, Payload payload) { + + if (initialRequestN < 1) { + throw new IllegalArgumentException("request n is less than 1"); + } + + int reqN = initialRequestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) initialRequestN; + + return GenericFrameCodec.encodeReleasingPayload( + allocator, FrameType.REQUEST_STREAM, streamId, false, false, reqN, payload); + } + + public static ByteBuf encode( + ByteBufAllocator allocator, + int streamId, + boolean fragmentFollows, + long initialRequestN, + ByteBuf metadata, + ByteBuf data) { + + if (initialRequestN < 1) { + throw new IllegalArgumentException("request n is less than 1"); + } + + int reqN = initialRequestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) initialRequestN; + + return GenericFrameCodec.encode( + allocator, + FrameType.REQUEST_STREAM, + streamId, + fragmentFollows, + false, + false, + reqN, + metadata, + data); + } + + public static ByteBuf data(ByteBuf byteBuf) { + return GenericFrameCodec.dataWithRequestN(byteBuf); + } + + public static ByteBuf metadata(ByteBuf byteBuf) { + return GenericFrameCodec.metadataWithRequestN(byteBuf); + } + + public static long initialRequestN(ByteBuf byteBuf) { + int requestN = GenericFrameCodec.initialRequestN(byteBuf); + return requestN == Integer.MAX_VALUE ? Long.MAX_VALUE : requestN; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java deleted file mode 100644 index 2fb209ffb..000000000 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameFlyweight.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.util.IllegalReferenceCountException; -import io.rsocket.Payload; - -public class RequestStreamFrameFlyweight { - - private static final RequestFlyweight FLYWEIGHT = new RequestFlyweight(FrameType.REQUEST_STREAM); - - private RequestStreamFrameFlyweight() {} - - public static ByteBuf encodeReleasingPayload( - ByteBufAllocator allocator, int streamId, long initialRequestN, Payload payload) { - - // if refCnt exceptions throws here it is safe to do no-op - boolean hasMetadata = payload.hasMetadata(); - // if refCnt exceptions throws here it is safe to do no-op still - final ByteBuf metadata = hasMetadata ? payload.metadata().retain() : null; - final ByteBuf data; - // retaining data safely. May throw either NPE or RefCntE - try { - data = payload.data().retain(); - } catch (IllegalReferenceCountException | NullPointerException e) { - if (hasMetadata) { - metadata.release(); - } - throw e; - } - // releasing payload safely since it can be already released wheres we have to release retained - // data and metadata as well - try { - payload.release(); - } catch (IllegalReferenceCountException e) { - data.release(); - if (hasMetadata) { - metadata.release(); - } - throw e; - } - - return encode(allocator, streamId, false, initialRequestN, metadata, data); - } - - public static ByteBuf encode( - ByteBufAllocator allocator, - int streamId, - boolean fragmentFollows, - long initialRequestN, - ByteBuf metadata, - ByteBuf data) { - - if (initialRequestN < 1) { - throw new IllegalArgumentException("request n is less than 1"); - } - - int reqN = initialRequestN > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) initialRequestN; - - return FLYWEIGHT.encode( - allocator, streamId, fragmentFollows, false, false, reqN, metadata, data); - } - - public static ByteBuf data(ByteBuf byteBuf) { - return FLYWEIGHT.dataWithRequestN(byteBuf); - } - - public static ByteBuf metadata(ByteBuf byteBuf) { - return FLYWEIGHT.metadataWithRequestN(byteBuf); - } - - public static long initialRequestN(ByteBuf byteBuf) { - int requestN = FLYWEIGHT.initialRequestN(byteBuf); - return requestN == Integer.MAX_VALUE ? Long.MAX_VALUE : requestN; - } -} diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameCodec.java similarity index 79% rename from rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameCodec.java index 06c9fc38c..aae89f7ab 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/ResumeFrameCodec.java @@ -21,8 +21,8 @@ import io.netty.buffer.Unpooled; import java.util.UUID; -public class ResumeFrameFlyweight { - static final int CURRENT_VERSION = SetupFrameFlyweight.CURRENT_VERSION; +public class ResumeFrameCodec { + static final int CURRENT_VERSION = SetupFrameCodec.CURRENT_VERSION; public static ByteBuf encode( ByteBufAllocator allocator, @@ -30,7 +30,7 @@ public static ByteBuf encode( long lastReceivedServerPos, long firstAvailableClientPos) { - ByteBuf byteBuf = FrameHeaderFlyweight.encodeStreamZero(allocator, FrameType.RESUME, 0); + ByteBuf byteBuf = FrameHeaderCodec.encodeStreamZero(allocator, FrameType.RESUME, 0); byteBuf.writeInt(CURRENT_VERSION); token.markReaderIndex(); byteBuf.writeShort(token.readableBytes()); @@ -43,10 +43,10 @@ public static ByteBuf encode( } public static int version(ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.RESUME, byteBuf); + FrameHeaderCodec.ensureFrameType(FrameType.RESUME, byteBuf); byteBuf.markReaderIndex(); - byteBuf.skipBytes(FrameHeaderFlyweight.size()); + byteBuf.skipBytes(FrameHeaderCodec.size()); int version = byteBuf.readInt(); byteBuf.resetReaderIndex(); @@ -54,11 +54,11 @@ public static int version(ByteBuf byteBuf) { } public static ByteBuf token(ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.RESUME, byteBuf); + FrameHeaderCodec.ensureFrameType(FrameType.RESUME, byteBuf); byteBuf.markReaderIndex(); // header + version - int tokenPos = FrameHeaderFlyweight.size() + Integer.BYTES; + int tokenPos = FrameHeaderCodec.size() + Integer.BYTES; byteBuf.skipBytes(tokenPos); // token int tokenLength = byteBuf.readShort() & 0xFFFF; @@ -69,11 +69,11 @@ public static ByteBuf token(ByteBuf byteBuf) { } public static long lastReceivedServerPos(ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.RESUME, byteBuf); + FrameHeaderCodec.ensureFrameType(FrameType.RESUME, byteBuf); byteBuf.markReaderIndex(); // header + version - int tokenPos = FrameHeaderFlyweight.size() + Integer.BYTES; + int tokenPos = FrameHeaderCodec.size() + Integer.BYTES; byteBuf.skipBytes(tokenPos); // token int tokenLength = byteBuf.readShort() & 0xFFFF; @@ -85,11 +85,11 @@ public static long lastReceivedServerPos(ByteBuf byteBuf) { } public static long firstAvailableClientPos(ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.RESUME, byteBuf); + FrameHeaderCodec.ensureFrameType(FrameType.RESUME, byteBuf); byteBuf.markReaderIndex(); // header + version - int tokenPos = FrameHeaderFlyweight.size() + Integer.BYTES; + int tokenPos = FrameHeaderCodec.size() + Integer.BYTES; byteBuf.skipBytes(tokenPos); // token int tokenLength = byteBuf.readShort() & 0xFFFF; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFrameCodec.java similarity index 67% rename from rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFrameFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFrameCodec.java index dd1971603..2b6951e49 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/ResumeOkFrameCodec.java @@ -3,18 +3,18 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -public class ResumeOkFrameFlyweight { +public class ResumeOkFrameCodec { public static ByteBuf encode(final ByteBufAllocator allocator, long lastReceivedClientPos) { - ByteBuf byteBuf = FrameHeaderFlyweight.encodeStreamZero(allocator, FrameType.RESUME_OK, 0); + ByteBuf byteBuf = FrameHeaderCodec.encodeStreamZero(allocator, FrameType.RESUME_OK, 0); byteBuf.writeLong(lastReceivedClientPos); return byteBuf; } public static long lastReceivedClientPos(ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.RESUME_OK, byteBuf); + FrameHeaderCodec.ensureFrameType(FrameType.RESUME_OK, byteBuf); byteBuf.markReaderIndex(); - long lastReceivedClientPosition = byteBuf.skipBytes(FrameHeaderFlyweight.size()).readLong(); + long lastReceivedClientPosition = byteBuf.skipBytes(FrameHeaderCodec.size()).readLong(); byteBuf.resetReaderIndex(); return lastReceivedClientPosition; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameCodec.java similarity index 83% rename from rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/SetupFrameCodec.java index bfb73fe22..d6f7431e4 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameCodec.java @@ -7,7 +7,7 @@ import io.rsocket.Payload; import java.nio.charset.StandardCharsets; -public class SetupFrameFlyweight { +public class SetupFrameCodec { /** * A flag used to indicate that the client requires connection resumption, if possible (the frame * contains a Resume Identification Token) @@ -17,9 +17,9 @@ public class SetupFrameFlyweight { /** A flag used to indicate that the client will honor LEASE sent by the server */ public static final int FLAGS_WILL_HONOR_LEASE = 0b00_0100_0000; - public static final int CURRENT_VERSION = VersionFlyweight.encode(1, 0); + public static final int CURRENT_VERSION = VersionCodec.encode(1, 0); - private static final int VERSION_FIELD_OFFSET = FrameHeaderFlyweight.size(); + private static final int VERSION_FIELD_OFFSET = FrameHeaderCodec.size(); private static final int KEEPALIVE_INTERVAL_FIELD_OFFSET = VERSION_FIELD_OFFSET + Integer.BYTES; private static final int KEEPALIVE_MAX_LIFETIME_FIELD_OFFSET = KEEPALIVE_INTERVAL_FIELD_OFFSET + Integer.BYTES; @@ -70,10 +70,10 @@ public static ByteBuf encode( } if (hasMetadata) { - flags |= FrameHeaderFlyweight.FLAGS_M; + flags |= FrameHeaderCodec.FLAGS_M; } - final ByteBuf header = FrameHeaderFlyweight.encodeStreamZero(allocator, FrameType.SETUP, flags); + final ByteBuf header = FrameHeaderCodec.encodeStreamZero(allocator, FrameType.SETUP, flags); header.writeInt(CURRENT_VERSION).writeInt(keepaliveInterval).writeInt(maxLifetime); @@ -93,11 +93,11 @@ public static ByteBuf encode( header.writeByte(length); ByteBufUtil.writeUtf8(header, dataMimeType); - return DataAndMetadataFlyweight.encode(allocator, header, metadata, hasMetadata, data); + return FrameBodyCodec.encode(allocator, header, metadata, hasMetadata, data); } public static int version(ByteBuf byteBuf) { - FrameHeaderFlyweight.ensureFrameType(FrameType.SETUP, byteBuf); + FrameHeaderCodec.ensureFrameType(FrameType.SETUP, byteBuf); byteBuf.markReaderIndex(); int version = byteBuf.skipBytes(VERSION_FIELD_OFFSET).readInt(); byteBuf.resetReaderIndex(); @@ -106,7 +106,7 @@ public static int version(ByteBuf byteBuf) { public static String humanReadableVersion(ByteBuf byteBuf) { int encodedVersion = version(byteBuf); - return VersionFlyweight.major(encodedVersion) + "." + VersionFlyweight.minor(encodedVersion); + return VersionCodec.major(encodedVersion) + "." + VersionCodec.minor(encodedVersion); } public static boolean isSupportedVersion(ByteBuf byteBuf) { @@ -135,11 +135,11 @@ public static int keepAliveMaxLifetime(ByteBuf byteBuf) { } public static boolean honorLease(ByteBuf byteBuf) { - return (FLAGS_WILL_HONOR_LEASE & FrameHeaderFlyweight.flags(byteBuf)) == FLAGS_WILL_HONOR_LEASE; + return (FLAGS_WILL_HONOR_LEASE & FrameHeaderCodec.flags(byteBuf)) == FLAGS_WILL_HONOR_LEASE; } public static boolean resumeEnabled(ByteBuf byteBuf) { - return (FLAGS_RESUME_ENABLE & FrameHeaderFlyweight.flags(byteBuf)) == FLAGS_RESUME_ENABLE; + return (FLAGS_RESUME_ENABLE & FrameHeaderCodec.flags(byteBuf)) == FLAGS_RESUME_ENABLE; } public static ByteBuf resumeToken(ByteBuf byteBuf) { @@ -147,7 +147,7 @@ public static ByteBuf resumeToken(ByteBuf byteBuf) { byteBuf.markReaderIndex(); // header int resumePos = - FrameHeaderFlyweight.size() + FrameHeaderCodec.size() + // version Integer.BYTES @@ -187,29 +187,29 @@ public static String dataMimeType(ByteBuf byteBuf) { } public static ByteBuf metadata(ByteBuf byteBuf) { - boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(byteBuf); + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); if (!hasMetadata) { return null; } byteBuf.markReaderIndex(); skipToPayload(byteBuf); - ByteBuf metadata = DataAndMetadataFlyweight.metadataWithoutMarking(byteBuf); + ByteBuf metadata = FrameBodyCodec.metadataWithoutMarking(byteBuf); byteBuf.resetReaderIndex(); return metadata; } public static ByteBuf data(ByteBuf byteBuf) { - boolean hasMetadata = FrameHeaderFlyweight.hasMetadata(byteBuf); + boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); byteBuf.markReaderIndex(); skipToPayload(byteBuf); - ByteBuf data = DataAndMetadataFlyweight.dataWithoutMarking(byteBuf, hasMetadata); + ByteBuf data = FrameBodyCodec.dataWithoutMarking(byteBuf, hasMetadata); byteBuf.resetReaderIndex(); return data; } private static int bytesToSkipToMimeType(ByteBuf byteBuf) { int bytesToSkip = VARIABLE_DATA_OFFSET; - if ((FLAGS_RESUME_ENABLE & FrameHeaderFlyweight.flags(byteBuf)) == FLAGS_RESUME_ENABLE) { + if ((FLAGS_RESUME_ENABLE & FrameHeaderCodec.flags(byteBuf)) == FLAGS_RESUME_ENABLE) { bytesToSkip += resumeTokenLength(byteBuf) + Short.BYTES; } return bytesToSkip; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/VersionFlyweight.java b/rsocket-core/src/main/java/io/rsocket/frame/VersionCodec.java similarity index 96% rename from rsocket-core/src/main/java/io/rsocket/frame/VersionFlyweight.java rename to rsocket-core/src/main/java/io/rsocket/frame/VersionCodec.java index e238b3fe2..35e4aa86a 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/VersionFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/VersionCodec.java @@ -16,7 +16,7 @@ package io.rsocket.frame; -public class VersionFlyweight { +public class VersionCodec { public static int encode(int major, int minor) { return (major << 16) | (minor & 0xFFFF); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java b/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java index 0a77e3820..e6874c097 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/decoder/DefaultPayloadDecoder.java @@ -3,14 +3,14 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.Payload; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; -import io.rsocket.frame.MetadataPushFrameFlyweight; -import io.rsocket.frame.PayloadFrameFlyweight; -import io.rsocket.frame.RequestChannelFrameFlyweight; -import io.rsocket.frame.RequestFireAndForgetFrameFlyweight; -import io.rsocket.frame.RequestResponseFrameFlyweight; -import io.rsocket.frame.RequestStreamFrameFlyweight; +import io.rsocket.frame.MetadataPushFrameCodec; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; import io.rsocket.util.DefaultPayload; import java.nio.ByteBuffer; @@ -21,32 +21,32 @@ class DefaultPayloadDecoder implements PayloadDecoder { public Payload apply(ByteBuf byteBuf) { ByteBuf m; ByteBuf d; - FrameType type = FrameHeaderFlyweight.frameType(byteBuf); + FrameType type = FrameHeaderCodec.frameType(byteBuf); switch (type) { case REQUEST_FNF: - d = RequestFireAndForgetFrameFlyweight.data(byteBuf); - m = RequestFireAndForgetFrameFlyweight.metadata(byteBuf); + d = RequestFireAndForgetFrameCodec.data(byteBuf); + m = RequestFireAndForgetFrameCodec.metadata(byteBuf); break; case REQUEST_RESPONSE: - d = RequestResponseFrameFlyweight.data(byteBuf); - m = RequestResponseFrameFlyweight.metadata(byteBuf); + d = RequestResponseFrameCodec.data(byteBuf); + m = RequestResponseFrameCodec.metadata(byteBuf); break; case REQUEST_STREAM: - d = RequestStreamFrameFlyweight.data(byteBuf); - m = RequestStreamFrameFlyweight.metadata(byteBuf); + d = RequestStreamFrameCodec.data(byteBuf); + m = RequestStreamFrameCodec.metadata(byteBuf); break; case REQUEST_CHANNEL: - d = RequestChannelFrameFlyweight.data(byteBuf); - m = RequestChannelFrameFlyweight.metadata(byteBuf); + d = RequestChannelFrameCodec.data(byteBuf); + m = RequestChannelFrameCodec.metadata(byteBuf); break; case NEXT: case NEXT_COMPLETE: - d = PayloadFrameFlyweight.data(byteBuf); - m = PayloadFrameFlyweight.metadata(byteBuf); + d = PayloadFrameCodec.data(byteBuf); + m = PayloadFrameCodec.metadata(byteBuf); break; case METADATA_PUSH: d = Unpooled.EMPTY_BUFFER; - m = MetadataPushFrameFlyweight.metadata(byteBuf); + m = MetadataPushFrameCodec.metadata(byteBuf); break; default: throw new IllegalArgumentException("unsupported frame type: " + type); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/decoder/ZeroCopyPayloadDecoder.java b/rsocket-core/src/main/java/io/rsocket/frame/decoder/ZeroCopyPayloadDecoder.java index c92f82428..3a0dc7bb5 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/decoder/ZeroCopyPayloadDecoder.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/decoder/ZeroCopyPayloadDecoder.java @@ -3,14 +3,14 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.Payload; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; -import io.rsocket.frame.MetadataPushFrameFlyweight; -import io.rsocket.frame.PayloadFrameFlyweight; -import io.rsocket.frame.RequestChannelFrameFlyweight; -import io.rsocket.frame.RequestFireAndForgetFrameFlyweight; -import io.rsocket.frame.RequestResponseFrameFlyweight; -import io.rsocket.frame.RequestStreamFrameFlyweight; +import io.rsocket.frame.MetadataPushFrameCodec; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; import io.rsocket.util.ByteBufPayload; /** @@ -22,32 +22,32 @@ public class ZeroCopyPayloadDecoder implements PayloadDecoder { public Payload apply(ByteBuf byteBuf) { ByteBuf m; ByteBuf d; - FrameType type = FrameHeaderFlyweight.frameType(byteBuf); + FrameType type = FrameHeaderCodec.frameType(byteBuf); switch (type) { case REQUEST_FNF: - d = RequestFireAndForgetFrameFlyweight.data(byteBuf); - m = RequestFireAndForgetFrameFlyweight.metadata(byteBuf); + d = RequestFireAndForgetFrameCodec.data(byteBuf); + m = RequestFireAndForgetFrameCodec.metadata(byteBuf); break; case REQUEST_RESPONSE: - d = RequestResponseFrameFlyweight.data(byteBuf); - m = RequestResponseFrameFlyweight.metadata(byteBuf); + d = RequestResponseFrameCodec.data(byteBuf); + m = RequestResponseFrameCodec.metadata(byteBuf); break; case REQUEST_STREAM: - d = RequestStreamFrameFlyweight.data(byteBuf); - m = RequestStreamFrameFlyweight.metadata(byteBuf); + d = RequestStreamFrameCodec.data(byteBuf); + m = RequestStreamFrameCodec.metadata(byteBuf); break; case REQUEST_CHANNEL: - d = RequestChannelFrameFlyweight.data(byteBuf); - m = RequestChannelFrameFlyweight.metadata(byteBuf); + d = RequestChannelFrameCodec.data(byteBuf); + m = RequestChannelFrameCodec.metadata(byteBuf); break; case NEXT: case NEXT_COMPLETE: - d = PayloadFrameFlyweight.data(byteBuf); - m = PayloadFrameFlyweight.metadata(byteBuf); + d = PayloadFrameCodec.data(byteBuf); + m = PayloadFrameCodec.metadata(byteBuf); break; case METADATA_PUSH: d = Unpooled.EMPTY_BUFFER; - m = MetadataPushFrameFlyweight.metadata(byteBuf); + m = MetadataPushFrameCodec.metadata(byteBuf); break; default: throw new IllegalArgumentException("unsupported frame type: " + type); diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index cf3eeb120..c294d6539 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -20,7 +20,7 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.Closeable; import io.rsocket.DuplexConnection; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameUtil; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; import io.rsocket.plugins.InitializingInterceptorRegistry; @@ -79,10 +79,10 @@ public ClientServerInputMultiplexer( .receive() .groupBy( frame -> { - int streamId = FrameHeaderFlyweight.streamId(frame); + int streamId = FrameHeaderCodec.streamId(frame); final Type type; if (streamId == 0) { - switch (FrameHeaderFlyweight.frameType(frame)) { + switch (FrameHeaderCodec.frameType(frame)) { case SETUP: case RESUME: case RESUME_OK: diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java index ea8a0de22..db29d8030 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/KeepAliveSupport.java @@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; -import io.rsocket.frame.KeepAliveFrameFlyweight; +import io.rsocket.frame.KeepAliveFrameCodec; import io.rsocket.resume.ResumeStateHolder; import java.time.Duration; import java.util.concurrent.atomic.AtomicBoolean; @@ -69,14 +69,14 @@ public void receive(ByteBuf keepAliveFrame) { long remoteLastReceivedPos = remoteLastReceivedPosition(keepAliveFrame); resumeStateHolder.onImpliedPosition(remoteLastReceivedPos); } - if (KeepAliveFrameFlyweight.respondFlag(keepAliveFrame)) { + if (KeepAliveFrameCodec.respondFlag(keepAliveFrame)) { long localLastReceivedPos = localLastReceivedPosition(); send( - KeepAliveFrameFlyweight.encode( + KeepAliveFrameCodec.encode( allocator, false, localLastReceivedPos, - KeepAliveFrameFlyweight.data(keepAliveFrame).retain())); + KeepAliveFrameCodec.data(keepAliveFrame).retain())); } } @@ -118,7 +118,7 @@ long localLastReceivedPosition() { } long remoteLastReceivedPosition(ByteBuf keepAliveFrame) { - return KeepAliveFrameFlyweight.lastPosition(keepAliveFrame); + return KeepAliveFrameCodec.lastPosition(keepAliveFrame); } public static final class ServerKeepAliveSupport extends KeepAliveSupport { @@ -145,7 +145,7 @@ public ClientKeepAliveSupport( void onIntervalTick() { tryTimeout(); send( - KeepAliveFrameFlyweight.encode( + KeepAliveFrameCodec.encode( allocator, true, localLastReceivedPosition(), Unpooled.EMPTY_BUFFER)); } } diff --git a/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java b/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java index dd4247090..fd569a2c8 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/RequesterLeaseHandler.java @@ -18,7 +18,7 @@ import io.netty.buffer.ByteBuf; import io.rsocket.Availability; -import io.rsocket.frame.LeaseFrameFlyweight; +import io.rsocket.frame.LeaseFrameCodec; import java.util.function.Consumer; import reactor.core.Disposable; import reactor.core.publisher.Flux; @@ -63,9 +63,9 @@ public Exception leaseError() { @Override public void receive(ByteBuf leaseFrame) { - int numberOfRequests = LeaseFrameFlyweight.numRequests(leaseFrame); - int timeToLiveMillis = LeaseFrameFlyweight.ttl(leaseFrame); - ByteBuf metadata = LeaseFrameFlyweight.metadata(leaseFrame); + int numberOfRequests = LeaseFrameCodec.numRequests(leaseFrame); + int timeToLiveMillis = LeaseFrameCodec.ttl(leaseFrame); + ByteBuf metadata = LeaseFrameCodec.metadata(leaseFrame); LeaseImpl lease = LeaseImpl.create(timeToLiveMillis, numberOfRequests, metadata); currentLease = lease; receivedLease.onNext(lease); diff --git a/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java b/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java index 5ca745ee7..5f000cb30 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java @@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.Availability; -import io.rsocket.frame.LeaseFrameFlyweight; +import io.rsocket.frame.LeaseFrameCodec; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Function; @@ -96,7 +96,7 @@ public double availability() { } private ByteBuf createLeaseFrame(Lease lease) { - return LeaseFrameFlyweight.encode( + return LeaseFrameCodec.encode( allocator, lease.getTimeToLiveMillis(), lease.getAllowedRequests(), lease.getMetadata()); } diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java b/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java new file mode 100644 index 000000000..41dafb33d --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java @@ -0,0 +1,335 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import io.netty.util.CharsetUtil; +import io.rsocket.util.CharByteBufUtil; + +public class AuthMetadataCodec { + + static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 + static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 + + static final int USERNAME_BYTES_LENGTH = 1; + static final int AUTH_TYPE_ID_LENGTH = 1; + + static final char[] EMPTY_CHARS_ARRAY = new char[0]; + + private AuthMetadataCodec() {} + + /** + * Encode a Authentication CompositeMetadata payload using custom authentication type + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param customAuthType the custom mime type to encode. + * @param metadata the metadata value to encode. + * @throws IllegalArgumentException in case of {@code customAuthType} is non US_ASCII string or + * empty string or its length is greater than 128 bytes + */ + public static ByteBuf encodeMetadata( + ByteBufAllocator allocator, String customAuthType, ByteBuf metadata) { + + int actualASCIILength = ByteBufUtil.utf8Bytes(customAuthType); + if (actualASCIILength != customAuthType.length()) { + throw new IllegalArgumentException("custom auth type must be US_ASCII characters only"); + } + if (actualASCIILength < 1 || actualASCIILength > 128) { + throw new IllegalArgumentException( + "custom auth type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + + int capacity = 1 + actualASCIILength; + ByteBuf headerBuffer = allocator.buffer(capacity, capacity); + // encoded length is one less than actual length, since 0 is never a valid length, which gives + // wider representation range + headerBuffer.writeByte(actualASCIILength - 1); + + ByteBufUtil.reserveAndWriteUtf8(headerBuffer, customAuthType, actualASCIILength); + + return allocator.compositeBuffer(2).addComponents(true, headerBuffer, metadata); + } + + /** + * Encode a Authentication CompositeMetadata payload using custom authentication type + * + * @param allocator the {@link ByteBufAllocator} to create intermediate buffers as needed. + * @param authType the well-known mime type to encode. + * @param metadata the metadata value to encode. + * @throws IllegalArgumentException in case of {@code authType} is {@link + * WellKnownAuthType#UNPARSEABLE_AUTH_TYPE} or {@link + * WellKnownAuthType#UNKNOWN_RESERVED_AUTH_TYPE} + */ + public static ByteBuf encodeMetadata( + ByteBufAllocator allocator, WellKnownAuthType authType, ByteBuf metadata) { + + if (authType == WellKnownAuthType.UNPARSEABLE_AUTH_TYPE + || authType == WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE) { + throw new IllegalArgumentException("only allowed AuthType should be used"); + } + + int capacity = AUTH_TYPE_ID_LENGTH; + ByteBuf headerBuffer = + allocator + .buffer(capacity, capacity) + .writeByte(authType.getIdentifier() | STREAM_METADATA_KNOWN_MASK); + + return allocator.compositeBuffer(2).addComponents(true, headerBuffer, metadata); + } + + /** + * Encode a Authentication CompositeMetadata payload using Simple Authentication format + * + * @throws IllegalArgumentException if the username length is greater than 255 + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param username the char sequence which represents user name. + * @param password the char sequence which represents user password. + */ + public static ByteBuf encodeSimpleMetadata( + ByteBufAllocator allocator, char[] username, char[] password) { + + int usernameLength = CharByteBufUtil.utf8Bytes(username); + if (usernameLength > 255) { + throw new IllegalArgumentException( + "Username should be shorter than or equal to 255 bytes length in UTF-8 encoding"); + } + + int passwordLength = CharByteBufUtil.utf8Bytes(password); + int capacity = AUTH_TYPE_ID_LENGTH + USERNAME_BYTES_LENGTH + usernameLength + passwordLength; + final ByteBuf buffer = + allocator + .buffer(capacity, capacity) + .writeByte(WellKnownAuthType.SIMPLE.getIdentifier() | STREAM_METADATA_KNOWN_MASK) + .writeByte(usernameLength); + + CharByteBufUtil.writeUtf8(buffer, username); + CharByteBufUtil.writeUtf8(buffer, password); + + return buffer; + } + + /** + * Encode a Authentication CompositeMetadata payload using Bearer Authentication format + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param token the char sequence which represents BEARER token. + */ + public static ByteBuf encodeBearerMetadata(ByteBufAllocator allocator, char[] token) { + + int tokenLength = CharByteBufUtil.utf8Bytes(token); + int capacity = AUTH_TYPE_ID_LENGTH + tokenLength; + final ByteBuf buffer = + allocator + .buffer(capacity, capacity) + .writeByte(WellKnownAuthType.BEARER.getIdentifier() | STREAM_METADATA_KNOWN_MASK); + + CharByteBufUtil.writeUtf8(buffer, token); + + return buffer; + } + + /** + * Encode a new Authentication Metadata payload information, first verifying if the passed {@link + * String} matches a {@link WellKnownAuthType} (in which case it will be encoded in a compressed + * fashion using the mime id of that type). + * + *

    Prefer using {@link #encodeMetadata(ByteBufAllocator, String, ByteBuf)} if you already know + * that the mime type is not a {@link WellKnownAuthType}. + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param authType the mime type to encode, as a {@link String}. well known mime types are + * compressed. + * @param metadata the metadata value to encode. + * @see #encodeMetadata(ByteBufAllocator, WellKnownAuthType, ByteBuf) + * @see #encodeMetadata(ByteBufAllocator, String, ByteBuf) + */ + public static ByteBuf encodeMetadataWithCompression( + ByteBufAllocator allocator, String authType, ByteBuf metadata) { + WellKnownAuthType wkn = WellKnownAuthType.fromString(authType); + if (wkn == WellKnownAuthType.UNPARSEABLE_AUTH_TYPE) { + return AuthMetadataCodec.encodeMetadata(allocator, authType, metadata); + } else { + return AuthMetadataCodec.encodeMetadata(allocator, wkn, metadata); + } + } + + /** + * Get the first {@code byte} from a {@link ByteBuf} and check whether it is length or {@link + * WellKnownAuthType}. Assuming said buffer properly contains such a {@code byte} + * + * @param metadata byteBuf used to get information from + */ + public static boolean isWellKnownAuthType(ByteBuf metadata) { + byte lengthOrId = metadata.getByte(0); + return (lengthOrId & STREAM_METADATA_LENGTH_MASK) != lengthOrId; + } + + /** + * Read first byte from the given {@code metadata} and tries to convert it's value to {@link + * WellKnownAuthType}. + * + * @param metadata given metadata buffer to read from + * @return Return on of the know Auth types or {@link WellKnownAuthType#UNPARSEABLE_AUTH_TYPE} if + * field's value is length or unknown auth type + * @throws IllegalStateException if not enough readable bytes in the given {@link ByteBuf} + */ + public static WellKnownAuthType decodeWellKnownAuthType(ByteBuf metadata) { + if (metadata.readableBytes() < 1) { + throw new IllegalStateException( + "Unable to decode Well Know Auth type. Not enough readable bytes"); + } + byte lengthOrId = metadata.readByte(); + int normalizedId = (byte) (lengthOrId & STREAM_METADATA_LENGTH_MASK); + + if (normalizedId != lengthOrId) { + return WellKnownAuthType.fromIdentifier(normalizedId); + } + + return WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; + } + + /** + * Read up to 129 bytes from the given metadata in order to get the custom Auth Type + * + * @param metadata + * @return + */ + public static CharSequence decodeCustomAuthType(ByteBuf metadata) { + if (metadata.readableBytes() < 2) { + throw new IllegalStateException( + "Unable to decode custom Auth type. Not enough readable bytes"); + } + + byte encodedLength = metadata.readByte(); + if (encodedLength < 0) { + throw new IllegalStateException( + "Unable to decode custom Auth type. Incorrect auth type length"); + } + + // encoded length is realLength - 1 in order to avoid intersection with 0x00 authtype + int realLength = encodedLength + 1; + if (metadata.readableBytes() < realLength) { + throw new IllegalArgumentException( + "Unable to decode custom Auth type. Malformed length or auth type string"); + } + + return metadata.readCharSequence(realLength, CharsetUtil.US_ASCII); + } + + /** + * Read all remaining {@code bytes} from the given {@link ByteBuf} and return sliced + * representation of a payload + * + * @param metadata metadata to get payload from. Please note, the {@code metadata#readIndex} + * should be set to the beginning of the payload bytes + * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if no bytes readable in the + * given one + */ + public static ByteBuf decodePayload(ByteBuf metadata) { + if (metadata.readableBytes() == 0) { + return Unpooled.EMPTY_BUFFER; + } + + return metadata.readSlice(metadata.readableBytes()); + } + + /** + * Read up to 257 {@code bytes} from the given {@link ByteBuf} where the first byte is username + * length and the subsequent number of bytes equal to decoded length + * + * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the username length byte + * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if username length is zero + */ + public static ByteBuf decodeUsername(ByteBuf simpleAuthMetadata) { + short usernameLength = decodeUsernameLength(simpleAuthMetadata); + + if (usernameLength == 0) { + return Unpooled.EMPTY_BUFFER; + } + + return simpleAuthMetadata.readSlice(usernameLength); + } + + /** + * Read all the remaining {@code byte}s from the given {@link ByteBuf} which represents user's + * password + * + * @param simpleAuthMetadata the given metadata to read password from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes + * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if password length is zero + */ + public static ByteBuf decodePassword(ByteBuf simpleAuthMetadata) { + if (simpleAuthMetadata.readableBytes() == 0) { + return Unpooled.EMPTY_BUFFER; + } + + return simpleAuthMetadata.readSlice(simpleAuthMetadata.readableBytes()); + } + /** + * Read up to 257 {@code bytes} from the given {@link ByteBuf} where the first byte is username + * length and the subsequent number of bytes equal to decoded length + * + * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the username length byte + * @return {@code char[]} which represents UTF-8 username + */ + public static char[] decodeUsernameAsCharArray(ByteBuf simpleAuthMetadata) { + short usernameLength = decodeUsernameLength(simpleAuthMetadata); + + if (usernameLength == 0) { + return EMPTY_CHARS_ARRAY; + } + + return CharByteBufUtil.readUtf8(simpleAuthMetadata, usernameLength); + } + + /** + * Read all the remaining {@code byte}s from the given {@link ByteBuf} which represents user's + * password + * + * @param simpleAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes + * @return {@code char[]} which represents UTF-8 password + */ + public static char[] decodePasswordAsCharArray(ByteBuf simpleAuthMetadata) { + if (simpleAuthMetadata.readableBytes() == 0) { + return EMPTY_CHARS_ARRAY; + } + + return CharByteBufUtil.readUtf8(simpleAuthMetadata, simpleAuthMetadata.readableBytes()); + } + + /** + * Read all the remaining {@code bytes} from the given {@link ByteBuf} where the first byte is + * username length and the subsequent number of bytes equal to decoded length + * + * @param bearerAuthMetadata the given metadata to read username from. Please note, the {@code + * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes + * @return {@code char[]} which represents UTF-8 password + */ + public static char[] decodeBearerTokenAsCharArray(ByteBuf bearerAuthMetadata) { + if (bearerAuthMetadata.readableBytes() == 0) { + return EMPTY_CHARS_ARRAY; + } + + return CharByteBufUtil.readUtf8(bearerAuthMetadata, bearerAuthMetadata.readableBytes()); + } + + private static short decodeUsernameLength(ByteBuf simpleAuthMetadata) { + if (simpleAuthMetadata.readableBytes() < 1) { + throw new IllegalStateException( + "Unable to decode custom username. Not enough readable bytes"); + } + + short usernameLength = simpleAuthMetadata.readUnsignedByte(); + + if (simpleAuthMetadata.readableBytes() < usernameLength) { + throw new IllegalArgumentException( + "Unable to decode username. Malformed username length or content"); + } + + return usernameLength; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataCodec.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataCodec.java new file mode 100644 index 000000000..5e00abba8 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataCodec.java @@ -0,0 +1,385 @@ +/* + * Copyright 2015-2018 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.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import io.netty.util.CharsetUtil; +import io.rsocket.util.NumberUtils; +import reactor.util.annotation.Nullable; + +/** + * A flyweight class that can be used to encode/decode composite metadata information to/from {@link + * ByteBuf}. This is intended for low-level efficient manipulation of such buffers. See {@link + * CompositeMetadata} for an Iterator-like approach to decoding entries. + */ +public class CompositeMetadataCodec { + + static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 + + static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 + + private CompositeMetadataCodec() {} + + public static int computeNextEntryIndex( + int currentEntryIndex, ByteBuf headerSlice, ByteBuf contentSlice) { + return currentEntryIndex + + headerSlice.readableBytes() // this includes the mime length byte + + 3 // 3 bytes of the content length, which are excluded from the slice + + contentSlice.readableBytes(); + } + + /** + * Decode the next metadata entry (a mime header + content pair of {@link ByteBuf}) from a {@link + * ByteBuf} that contains at least enough bytes for one more such entry. These buffers are + * actually slices of the full metadata buffer, and this method doesn't move the full metadata + * buffer's {@link ByteBuf#readerIndex()}. As such, it requires the user to provide an {@code + * index} to read from. The next index is computed by calling {@link #computeNextEntryIndex(int, + * ByteBuf, ByteBuf)}. Size of the first buffer (the "header buffer") drives which decoding method + * should be further applied to it. + * + *

    The header buffer is either: + * + *

      + *
    • made up of a single byte: this represents an encoded mime id, which can be further + * decoded using {@link #decodeMimeIdFromMimeBuffer(ByteBuf)} + *
    • made up of 2 or more bytes: this represents an encoded mime String + its length, which + * can be further decoded using {@link #decodeMimeTypeFromMimeBuffer(ByteBuf)}. Note the + * encoded length, in the first byte, is skipped by this decoding method because the + * remaining length of the buffer is that of the mime string. + *
    + * + * @param compositeMetadata the source {@link ByteBuf} that originally contains one or more + * metadata entries + * @param entryIndex the {@link ByteBuf#readerIndex()} to start decoding from. original reader + * index is kept on the source buffer + * @param retainSlices should produced metadata entry buffers {@link ByteBuf#slice() slices} be + * {@link ByteBuf#retainedSlice() retained}? + * @return a {@link ByteBuf} array of length 2 containing the mime header buffer + * slice and the content buffer slice, or one of the + * zero-length error constant arrays + */ + public static ByteBuf[] decodeMimeAndContentBuffersSlices( + ByteBuf compositeMetadata, int entryIndex, boolean retainSlices) { + compositeMetadata.markReaderIndex(); + compositeMetadata.readerIndex(entryIndex); + + if (compositeMetadata.isReadable()) { + ByteBuf mime; + int ridx = compositeMetadata.readerIndex(); + byte mimeIdOrLength = compositeMetadata.readByte(); + if ((mimeIdOrLength & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK) { + mime = + retainSlices + ? compositeMetadata.retainedSlice(ridx, 1) + : compositeMetadata.slice(ridx, 1); + } else { + // M flag unset, remaining 7 bits are the length of the mime + int mimeLength = Byte.toUnsignedInt(mimeIdOrLength) + 1; + + if (compositeMetadata.isReadable( + mimeLength)) { // need to be able to read an extra mimeLength bytes + // here we need a way for the returned ByteBuf to differentiate between a + // 1-byte length mime type and a 1 byte encoded mime id, preferably without + // re-applying the byte mask. The easiest way is to include the initial byte + // and have further decoding ignore the first byte. 1 byte buffer == id, 2+ byte + // buffer == full mime string. + mime = + retainSlices + ? + // we accommodate that we don't read from current readerIndex, but + // readerIndex - 1 ("0"), for a total slice size of mimeLength + 1 + compositeMetadata.retainedSlice(ridx, mimeLength + 1) + : compositeMetadata.slice(ridx, mimeLength + 1); + // we thus need to skip the bytes we just sliced, but not the flag/length byte + // which was already skipped in initial read + compositeMetadata.skipBytes(mimeLength); + } else { + compositeMetadata.resetReaderIndex(); + throw new IllegalStateException("metadata is malformed"); + } + } + + if (compositeMetadata.isReadable(3)) { + // ensures the length medium can be read + final int metadataLength = compositeMetadata.readUnsignedMedium(); + if (compositeMetadata.isReadable(metadataLength)) { + ByteBuf metadata = + retainSlices + ? compositeMetadata.readRetainedSlice(metadataLength) + : compositeMetadata.readSlice(metadataLength); + compositeMetadata.resetReaderIndex(); + return new ByteBuf[] {mime, metadata}; + } else { + compositeMetadata.resetReaderIndex(); + throw new IllegalStateException("metadata is malformed"); + } + } else { + compositeMetadata.resetReaderIndex(); + throw new IllegalStateException("metadata is malformed"); + } + } + compositeMetadata.resetReaderIndex(); + throw new IllegalArgumentException( + String.format("entry index %d is larger than buffer size", entryIndex)); + } + + /** + * Decode a {@code byte} compressed mime id from a {@link ByteBuf}, assuming said buffer properly + * contains such an id. + * + *

    The buffer must have exactly one readable byte, which is assumed to have been tested for + * mime id encoding via the {@link #STREAM_METADATA_KNOWN_MASK} mask ({@code firstByte & + * STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK}). + * + *

    If there is no readable byte, the negative identifier of {@link + * WellKnownMimeType#UNPARSEABLE_MIME_TYPE} is returned. + * + * @param mimeBuffer the buffer that should next contain the compressed mime id byte + * @return the compressed mime id, between 0 and 127, or a negative id if the input is invalid + * @see #decodeMimeTypeFromMimeBuffer(ByteBuf) + */ + public static byte decodeMimeIdFromMimeBuffer(ByteBuf mimeBuffer) { + if (mimeBuffer.readableBytes() != 1) { + return WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier(); + } + return (byte) (mimeBuffer.readByte() & STREAM_METADATA_LENGTH_MASK); + } + + /** + * Decode a {@link CharSequence} custome mime type from a {@link ByteBuf}, assuming said buffer + * properly contains such a mime type. + * + *

    The buffer must at least have two readable bytes, which distinguishes it from the {@link + * #decodeMimeIdFromMimeBuffer(ByteBuf) compressed id} case. The first byte is a size and the + * remaining bytes must correspond to the {@link CharSequence}, encoded fully in US_ASCII. As a + * result, the first byte can simply be skipped, and the remaining of the buffer be decoded to the + * mime type. + * + *

    If the mime header buffer is less than 2 bytes long, returns {@code null}. + * + * @param flyweightMimeBuffer the mime header {@link ByteBuf} that contains length + custom mime + * type + * @return the decoded custom mime type, as a {@link CharSequence}, or null if the input is + * invalid + * @see #decodeMimeIdFromMimeBuffer(ByteBuf) + */ + @Nullable + public static CharSequence decodeMimeTypeFromMimeBuffer(ByteBuf flyweightMimeBuffer) { + if (flyweightMimeBuffer.readableBytes() < 2) { + throw new IllegalStateException("unable to decode explicit MIME type"); + } + // the encoded length is assumed to be kept at the start of the buffer + // but also assumed to be irrelevant because the rest of the slice length + // actually already matches _decoded_length + flyweightMimeBuffer.skipBytes(1); + int mimeStringLength = flyweightMimeBuffer.readableBytes(); + return flyweightMimeBuffer.readCharSequence(mimeStringLength, CharsetUtil.US_ASCII); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}, without checking if the {@link String} can be matched with a well known compressable + * mime type. Prefer using this method and {@link #encodeAndAddMetadata(CompositeByteBuf, + * ByteBufAllocator, WellKnownMimeType, ByteBuf)} if you know in advance whether or not the mime + * is well known. Otherwise use {@link #encodeAndAddMetadataWithCompression(CompositeByteBuf, + * ByteBufAllocator, String, ByteBuf)} + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param customMimeType the custom mime type to encode. + * @param metadata the metadata value to encode. + */ + // see #encodeMetadataHeader(ByteBufAllocator, String, int) + public static void encodeAndAddMetadata( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + String customMimeType, + ByteBuf metadata) { + compositeMetaData.addComponents( + true, encodeMetadataHeader(allocator, customMimeType, metadata.readableBytes()), metadata); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}. + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param knownMimeType the {@link WellKnownMimeType} to encode. + * @param metadata the metadata value to encode. + */ + // see #encodeMetadataHeader(ByteBufAllocator, byte, int) + public static void encodeAndAddMetadata( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + WellKnownMimeType knownMimeType, + ByteBuf metadata) { + compositeMetaData.addComponents( + true, + encodeMetadataHeader(allocator, knownMimeType.getIdentifier(), metadata.readableBytes()), + metadata); + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}, first verifying if the passed {@link String} matches a {@link WellKnownMimeType} (in + * which case it will be encoded in a compressed fashion using the mime id of that type). + * + *

    Prefer using {@link #encodeAndAddMetadata(CompositeByteBuf, ByteBufAllocator, String, + * ByteBuf)} if you already know that the mime type is not a {@link WellKnownMimeType}. + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param mimeType the mime type to encode, as a {@link String}. well known mime types are + * compressed. + * @param metadata the metadata value to encode. + * @see #encodeAndAddMetadata(CompositeByteBuf, ByteBufAllocator, WellKnownMimeType, ByteBuf) + */ + // see #encodeMetadataHeader(ByteBufAllocator, String, int) + public static void encodeAndAddMetadataWithCompression( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + String mimeType, + ByteBuf metadata) { + WellKnownMimeType wkn = WellKnownMimeType.fromString(mimeType); + if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { + compositeMetaData.addComponents( + true, encodeMetadataHeader(allocator, mimeType, metadata.readableBytes()), metadata); + } else { + compositeMetaData.addComponents( + true, + encodeMetadataHeader(allocator, wkn.getIdentifier(), metadata.readableBytes()), + metadata); + } + } + + /** + * Returns whether there is another entry available at a given index + * + * @param compositeMetadata the buffer to inspect + * @param entryIndex the index to check at + * @return whether there is another entry available at a given index + */ + public static boolean hasEntry(ByteBuf compositeMetadata, int entryIndex) { + return compositeMetadata.writerIndex() - entryIndex > 0; + } + + /** + * Returns whether the header represents a well-known MIME type. + * + * @param header the header to inspect + * @return whether the header represents a well-known MIME type + */ + public static boolean isWellKnownMimeType(ByteBuf header) { + return header.readableBytes() == 1; + } + + /** + * Encode a new sub-metadata information into a composite metadata {@link CompositeByteBuf + * buffer}. + * + * @param compositeMetaData the buffer that will hold all composite metadata information. + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param unknownCompressedMimeType the id of the {@link + * WellKnownMimeType#UNKNOWN_RESERVED_MIME_TYPE} to encode. + * @param metadata the metadata value to encode. + */ + // see #encodeMetadataHeader(ByteBufAllocator, byte, int) + static void encodeAndAddMetadata( + CompositeByteBuf compositeMetaData, + ByteBufAllocator allocator, + byte unknownCompressedMimeType, + ByteBuf metadata) { + compositeMetaData.addComponents( + true, + encodeMetadataHeader(allocator, unknownCompressedMimeType, metadata.readableBytes()), + metadata); + } + + /** + * Encode a custom mime type and a metadata value length into a newly allocated {@link ByteBuf}. + * + *

    This larger representation encodes the mime type representation's length on a single byte, + * then the representation itself, then the unsigned metadata value length on 3 additional bytes. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer. + * @param customMime a custom mime type to encode. + * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits + * integer. + * @return the encoded mime and metadata length information + */ + static ByteBuf encodeMetadataHeader( + ByteBufAllocator allocator, String customMime, int metadataLength) { + ByteBuf metadataHeader = allocator.buffer(4 + customMime.length()); + // reserve 1 byte for the customMime length + // /!\ careful not to read that first byte, which is random at this point + int writerIndexInitial = metadataHeader.writerIndex(); + metadataHeader.writerIndex(writerIndexInitial + 1); + + // write the custom mime in UTF8 but validate it is all ASCII-compatible + // (which produces the right result since ASCII chars are still encoded on 1 byte in UTF8) + int customMimeLength = ByteBufUtil.writeUtf8(metadataHeader, customMime); + if (!ByteBufUtil.isText( + metadataHeader, metadataHeader.readerIndex() + 1, customMimeLength, CharsetUtil.US_ASCII)) { + metadataHeader.release(); + throw new IllegalArgumentException("custom mime type must be US_ASCII characters only"); + } + if (customMimeLength < 1 || customMimeLength > 128) { + metadataHeader.release(); + throw new IllegalArgumentException( + "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); + } + metadataHeader.markWriterIndex(); + + // go back to beginning and write the length + // encoded length is one less than actual length, since 0 is never a valid length, which gives + // wider representation range + metadataHeader.writerIndex(writerIndexInitial); + metadataHeader.writeByte(customMimeLength - 1); + + // go back to post-mime type and write the metadata content length + metadataHeader.resetWriterIndex(); + NumberUtils.encodeUnsignedMedium(metadataHeader, metadataLength); + + return metadataHeader; + } + + /** + * Encode a {@link WellKnownMimeType well known mime type} and a metadata value length into a + * newly allocated {@link ByteBuf}. + * + *

    This compact representation encodes the mime type via its ID on a single byte, and the + * unsigned value length on 3 additional bytes. + * + * @param allocator the {@link ByteBufAllocator} to use to create the buffer. + * @param mimeType a byte identifier of a {@link WellKnownMimeType} to encode. + * @param metadataLength the metadata length to append to the buffer as an unsigned 24 bits + * integer. + * @return the encoded mime and metadata length information + */ + static ByteBuf encodeMetadataHeader( + ByteBufAllocator allocator, byte mimeType, int metadataLength) { + ByteBuf buffer = allocator.buffer(4, 4).writeByte(mimeType | STREAM_METADATA_KNOWN_MASK); + + NumberUtils.encodeUnsignedMedium(buffer, metadataLength); + + return buffer; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java index 0520285c2..9916dfd3b 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/CompositeMetadataFlyweight.java @@ -18,31 +18,25 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; import io.netty.buffer.CompositeByteBuf; -import io.netty.util.CharsetUtil; -import io.rsocket.util.NumberUtils; import reactor.util.annotation.Nullable; /** * A flyweight class that can be used to encode/decode composite metadata information to/from {@link * ByteBuf}. This is intended for low-level efficient manipulation of such buffers. See {@link * CompositeMetadata} for an Iterator-like approach to decoding entries. + * + * @deprecated in favor of {@link CompositeMetadataCodec} */ +@Deprecated public class CompositeMetadataFlyweight { - static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 - - static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 - private CompositeMetadataFlyweight() {} public static int computeNextEntryIndex( int currentEntryIndex, ByteBuf headerSlice, ByteBuf contentSlice) { - return currentEntryIndex - + headerSlice.readableBytes() // this includes the mime length byte - + 3 // 3 bytes of the content length, which are excluded from the slice - + contentSlice.readableBytes(); + return CompositeMetadataCodec.computeNextEntryIndex( + currentEntryIndex, headerSlice, contentSlice); } /** @@ -77,67 +71,8 @@ public static int computeNextEntryIndex( */ public static ByteBuf[] decodeMimeAndContentBuffersSlices( ByteBuf compositeMetadata, int entryIndex, boolean retainSlices) { - compositeMetadata.markReaderIndex(); - compositeMetadata.readerIndex(entryIndex); - - if (compositeMetadata.isReadable()) { - ByteBuf mime; - int ridx = compositeMetadata.readerIndex(); - byte mimeIdOrLength = compositeMetadata.readByte(); - if ((mimeIdOrLength & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK) { - mime = - retainSlices - ? compositeMetadata.retainedSlice(ridx, 1) - : compositeMetadata.slice(ridx, 1); - } else { - // M flag unset, remaining 7 bits are the length of the mime - int mimeLength = Byte.toUnsignedInt(mimeIdOrLength) + 1; - - if (compositeMetadata.isReadable( - mimeLength)) { // need to be able to read an extra mimeLength bytes - // here we need a way for the returned ByteBuf to differentiate between a - // 1-byte length mime type and a 1 byte encoded mime id, preferably without - // re-applying the byte mask. The easiest way is to include the initial byte - // and have further decoding ignore the first byte. 1 byte buffer == id, 2+ byte - // buffer == full mime string. - mime = - retainSlices - ? - // we accommodate that we don't read from current readerIndex, but - // readerIndex - 1 ("0"), for a total slice size of mimeLength + 1 - compositeMetadata.retainedSlice(ridx, mimeLength + 1) - : compositeMetadata.slice(ridx, mimeLength + 1); - // we thus need to skip the bytes we just sliced, but not the flag/length byte - // which was already skipped in initial read - compositeMetadata.skipBytes(mimeLength); - } else { - compositeMetadata.resetReaderIndex(); - throw new IllegalStateException("metadata is malformed"); - } - } - - if (compositeMetadata.isReadable(3)) { - // ensures the length medium can be read - final int metadataLength = compositeMetadata.readUnsignedMedium(); - if (compositeMetadata.isReadable(metadataLength)) { - ByteBuf metadata = - retainSlices - ? compositeMetadata.readRetainedSlice(metadataLength) - : compositeMetadata.readSlice(metadataLength); - compositeMetadata.resetReaderIndex(); - return new ByteBuf[] {mime, metadata}; - } else { - compositeMetadata.resetReaderIndex(); - throw new IllegalStateException("metadata is malformed"); - } - } else { - compositeMetadata.resetReaderIndex(); - throw new IllegalStateException("metadata is malformed"); - } - } - compositeMetadata.resetReaderIndex(); - throw new IllegalArgumentException( - String.format("entry index %d is larger than buffer size", entryIndex)); + return CompositeMetadataCodec.decodeMimeAndContentBuffersSlices( + compositeMetadata, entryIndex, retainSlices); } /** @@ -145,8 +80,8 @@ public static ByteBuf[] decodeMimeAndContentBuffersSlices( * contains such an id. * *

    The buffer must have exactly one readable byte, which is assumed to have been tested for - * mime id encoding via the {@link #STREAM_METADATA_KNOWN_MASK} mask ({@code firstByte & - * STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK}). + * mime id encoding via the {@link CompositeMetadataCodec#STREAM_METADATA_KNOWN_MASK} mask ({@code + * firstByte & STREAM_METADATA_KNOWN_MASK) == STREAM_METADATA_KNOWN_MASK}). * *

    If there is no readable byte, the negative identifier of {@link * WellKnownMimeType#UNPARSEABLE_MIME_TYPE} is returned. @@ -156,10 +91,7 @@ public static ByteBuf[] decodeMimeAndContentBuffersSlices( * @see #decodeMimeTypeFromMimeBuffer(ByteBuf) */ public static byte decodeMimeIdFromMimeBuffer(ByteBuf mimeBuffer) { - if (mimeBuffer.readableBytes() != 1) { - return WellKnownMimeType.UNPARSEABLE_MIME_TYPE.getIdentifier(); - } - return (byte) (mimeBuffer.readByte() & STREAM_METADATA_LENGTH_MASK); + return CompositeMetadataCodec.decodeMimeIdFromMimeBuffer(mimeBuffer); } /** @@ -182,15 +114,7 @@ public static byte decodeMimeIdFromMimeBuffer(ByteBuf mimeBuffer) { */ @Nullable public static CharSequence decodeMimeTypeFromMimeBuffer(ByteBuf flyweightMimeBuffer) { - if (flyweightMimeBuffer.readableBytes() < 2) { - throw new IllegalStateException("unable to decode explicit MIME type"); - } - // the encoded length is assumed to be kept at the start of the buffer - // but also assumed to be irrelevant because the rest of the slice length - // actually already matches _decoded_length - flyweightMimeBuffer.skipBytes(1); - int mimeStringLength = flyweightMimeBuffer.readableBytes(); - return flyweightMimeBuffer.readCharSequence(mimeStringLength, CharsetUtil.US_ASCII); + return CompositeMetadataCodec.decodeMimeTypeFromMimeBuffer(flyweightMimeBuffer); } /** @@ -212,8 +136,8 @@ public static void encodeAndAddMetadata( ByteBufAllocator allocator, String customMimeType, ByteBuf metadata) { - compositeMetaData.addComponents( - true, encodeMetadataHeader(allocator, customMimeType, metadata.readableBytes()), metadata); + CompositeMetadataCodec.encodeAndAddMetadata( + compositeMetaData, allocator, customMimeType, metadata); } /** @@ -231,10 +155,8 @@ public static void encodeAndAddMetadata( ByteBufAllocator allocator, WellKnownMimeType knownMimeType, ByteBuf metadata) { - compositeMetaData.addComponents( - true, - encodeMetadataHeader(allocator, knownMimeType.getIdentifier(), metadata.readableBytes()), - metadata); + CompositeMetadataCodec.encodeAndAddMetadata( + compositeMetaData, allocator, knownMimeType, metadata); } /** @@ -258,16 +180,8 @@ public static void encodeAndAddMetadataWithCompression( ByteBufAllocator allocator, String mimeType, ByteBuf metadata) { - WellKnownMimeType wkn = WellKnownMimeType.fromString(mimeType); - if (wkn == WellKnownMimeType.UNPARSEABLE_MIME_TYPE) { - compositeMetaData.addComponents( - true, encodeMetadataHeader(allocator, mimeType, metadata.readableBytes()), metadata); - } else { - compositeMetaData.addComponents( - true, - encodeMetadataHeader(allocator, wkn.getIdentifier(), metadata.readableBytes()), - metadata); - } + CompositeMetadataCodec.encodeAndAddMetadataWithCompression( + compositeMetaData, allocator, mimeType, metadata); } /** @@ -278,7 +192,7 @@ public static void encodeAndAddMetadataWithCompression( * @return whether there is another entry available at a given index */ public static boolean hasEntry(ByteBuf compositeMetadata, int entryIndex) { - return compositeMetadata.writerIndex() - entryIndex > 0; + return CompositeMetadataCodec.hasEntry(compositeMetadata, entryIndex); } /** @@ -288,7 +202,7 @@ public static boolean hasEntry(ByteBuf compositeMetadata, int entryIndex) { * @return whether the header represents a well-known MIME type */ public static boolean isWellKnownMimeType(ByteBuf header) { - return header.readableBytes() == 1; + return CompositeMetadataCodec.isWellKnownMimeType(header); } /** @@ -307,10 +221,8 @@ static void encodeAndAddMetadata( ByteBufAllocator allocator, byte unknownCompressedMimeType, ByteBuf metadata) { - compositeMetaData.addComponents( - true, - encodeMetadataHeader(allocator, unknownCompressedMimeType, metadata.readableBytes()), - metadata); + CompositeMetadataCodec.encodeAndAddMetadata( + compositeMetaData, allocator, unknownCompressedMimeType, metadata); } /** @@ -327,38 +239,7 @@ static void encodeAndAddMetadata( */ static ByteBuf encodeMetadataHeader( ByteBufAllocator allocator, String customMime, int metadataLength) { - ByteBuf metadataHeader = allocator.buffer(4 + customMime.length()); - // reserve 1 byte for the customMime length - // /!\ careful not to read that first byte, which is random at this point - int writerIndexInitial = metadataHeader.writerIndex(); - metadataHeader.writerIndex(writerIndexInitial + 1); - - // write the custom mime in UTF8 but validate it is all ASCII-compatible - // (which produces the right result since ASCII chars are still encoded on 1 byte in UTF8) - int customMimeLength = ByteBufUtil.writeUtf8(metadataHeader, customMime); - if (!ByteBufUtil.isText( - metadataHeader, metadataHeader.readerIndex() + 1, customMimeLength, CharsetUtil.US_ASCII)) { - metadataHeader.release(); - throw new IllegalArgumentException("custom mime type must be US_ASCII characters only"); - } - if (customMimeLength < 1 || customMimeLength > 128) { - metadataHeader.release(); - throw new IllegalArgumentException( - "custom mime type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); - } - metadataHeader.markWriterIndex(); - - // go back to beginning and write the length - // encoded length is one less than actual length, since 0 is never a valid length, which gives - // wider representation range - metadataHeader.writerIndex(writerIndexInitial); - metadataHeader.writeByte(customMimeLength - 1); - - // go back to post-mime type and write the metadata content length - metadataHeader.resetWriterIndex(); - NumberUtils.encodeUnsignedMedium(metadataHeader, metadataLength); - - return metadataHeader; + return CompositeMetadataCodec.encodeMetadataHeader(allocator, customMime, metadataLength); } /** @@ -376,10 +257,6 @@ static ByteBuf encodeMetadataHeader( */ static ByteBuf encodeMetadataHeader( ByteBufAllocator allocator, byte mimeType, int metadataLength) { - ByteBuf buffer = allocator.buffer(4, 4).writeByte(mimeType | STREAM_METADATA_KNOWN_MASK); - - NumberUtils.encodeUnsignedMedium(buffer, metadataLength); - - return buffer; + return CompositeMetadataCodec.encodeMetadataHeader(allocator, mimeType, metadataLength); } } diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataCodec.java b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataCodec.java new file mode 100644 index 000000000..d766cf59f --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataCodec.java @@ -0,0 +1,76 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.CompositeByteBuf; +import java.nio.charset.StandardCharsets; +import java.util.Collection; + +/** + * A flyweight class that can be used to encode/decode tagging metadata information to/from {@link + * ByteBuf}. This is intended for low-level efficient manipulation of such buffers. See {@link + * TaggingMetadata} for an Iterator-like approach to decoding entries. + * + * @author linux_china + */ +public class TaggingMetadataCodec { + /** Tag max length in bytes */ + private static int TAG_LENGTH_MAX = 0xFF; + + /** + * create routing metadata + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param tags tag values + * @return routing metadata + */ + public static RoutingMetadata createRoutingMetadata( + ByteBufAllocator allocator, Collection tags) { + return new RoutingMetadata(createTaggingContent(allocator, tags)); + } + + /** + * create tagging metadata from composite metadata entry + * + * @param entry composite metadata entry + * @return tagging metadata + */ + public static TaggingMetadata createTaggingMetadata(CompositeMetadata.Entry entry) { + return new TaggingMetadata(entry.getMimeType(), entry.getContent()); + } + + /** + * create tagging metadata + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param knownMimeType the {@link WellKnownMimeType} to encode. + * @param tags tag values + * @return Tagging Metadata + */ + public static TaggingMetadata createTaggingMetadata( + ByteBufAllocator allocator, String knownMimeType, Collection tags) { + return new TaggingMetadata(knownMimeType, createTaggingContent(allocator, tags)); + } + + /** + * create tagging content + * + * @param allocator the {@link ByteBufAllocator} to use to create intermediate buffers as needed. + * @param tags tag values + * @return tagging content + */ + public static ByteBuf createTaggingContent(ByteBufAllocator allocator, Collection tags) { + CompositeByteBuf taggingContent = allocator.compositeBuffer(); + for (String key : tags) { + int length = ByteBufUtil.utf8Bytes(key); + if (length == 0 || length > TAG_LENGTH_MAX) { + continue; + } + ByteBuf byteBuf = allocator.buffer().writeByte(length); + byteBuf.writeCharSequence(key, StandardCharsets.UTF_8); + taggingContent.addComponent(true, byteBuf); + } + return taggingContent; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataFlyweight.java index c7870bf0d..718528358 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/TaggingMetadataFlyweight.java @@ -2,9 +2,6 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; -import io.netty.buffer.CompositeByteBuf; -import java.nio.charset.StandardCharsets; import java.util.Collection; /** @@ -12,12 +9,11 @@ * ByteBuf}. This is intended for low-level efficient manipulation of such buffers. See {@link * TaggingMetadata} for an Iterator-like approach to decoding entries. * + * @deprecated in favor of {@link TaggingMetadataCodec} * @author linux_china */ +@Deprecated public class TaggingMetadataFlyweight { - /** Tag max length in bytes */ - private static int TAG_LENGTH_MAX = 0xFF; - /** * create routing metadata * @@ -27,7 +23,7 @@ public class TaggingMetadataFlyweight { */ public static RoutingMetadata createRoutingMetadata( ByteBufAllocator allocator, Collection tags) { - return new RoutingMetadata(createTaggingContent(allocator, tags)); + return TaggingMetadataCodec.createRoutingMetadata(allocator, tags); } /** @@ -37,7 +33,7 @@ public static RoutingMetadata createRoutingMetadata( * @return tagging metadata */ public static TaggingMetadata createTaggingMetadata(CompositeMetadata.Entry entry) { - return new TaggingMetadata(entry.getMimeType(), entry.getContent()); + return TaggingMetadataCodec.createTaggingMetadata(entry); } /** @@ -50,7 +46,7 @@ public static TaggingMetadata createTaggingMetadata(CompositeMetadata.Entry entr */ public static TaggingMetadata createTaggingMetadata( ByteBufAllocator allocator, String knownMimeType, Collection tags) { - return new TaggingMetadata(knownMimeType, createTaggingContent(allocator, tags)); + return TaggingMetadataCodec.createTaggingMetadata(allocator, knownMimeType, tags); } /** @@ -61,16 +57,6 @@ public static TaggingMetadata createTaggingMetadata( * @return tagging content */ public static ByteBuf createTaggingContent(ByteBufAllocator allocator, Collection tags) { - CompositeByteBuf taggingContent = allocator.compositeBuffer(); - for (String key : tags) { - int length = ByteBufUtil.utf8Bytes(key); - if (length == 0 || length > TAG_LENGTH_MAX) { - continue; - } - ByteBuf byteBuf = allocator.buffer().writeByte(length); - byteBuf.writeCharSequence(key, StandardCharsets.UTF_8); - taggingContent.addComponent(true, byteBuf); - } - return taggingContent; + return TaggingMetadataCodec.createTaggingContent(allocator, tags); } } diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownAuthType.java b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownAuthType.java new file mode 100644 index 000000000..66c98701c --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/WellKnownAuthType.java @@ -0,0 +1,121 @@ +/* + * Copyright 2015-2018 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.metadata; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Enumeration of Well Known Auth Types, as defined in the eponymous extension. Such auth types are + * used in composite metadata (which can include routing and/or tracing metadata). Per + * specification, identifiers are between 0 and 127 (inclusive). + */ +public enum WellKnownAuthType { + UNPARSEABLE_AUTH_TYPE("UNPARSEABLE_AUTH_TYPE_DO_NOT_USE", (byte) -2), + UNKNOWN_RESERVED_AUTH_TYPE("UNKNOWN_YET_RESERVED_DO_NOT_USE", (byte) -1), + + SIMPLE("simple", (byte) 0x00), + BEARER("bearer", (byte) 0x01); + // ... reserved for future use ... + + static final WellKnownAuthType[] TYPES_BY_AUTH_ID; + static final Map TYPES_BY_AUTH_STRING; + + static { + // precompute an array of all valid auth ids, filling the blanks with the RESERVED enum + TYPES_BY_AUTH_ID = new WellKnownAuthType[128]; // 0-127 inclusive + Arrays.fill(TYPES_BY_AUTH_ID, UNKNOWN_RESERVED_AUTH_TYPE); + // also prepare a Map of the types by auth string + TYPES_BY_AUTH_STRING = new LinkedHashMap<>(128); + + for (WellKnownAuthType value : values()) { + if (value.getIdentifier() >= 0) { + TYPES_BY_AUTH_ID[value.getIdentifier()] = value; + TYPES_BY_AUTH_STRING.put(value.getString(), value); + } + } + } + + private final byte identifier; + private final String str; + + WellKnownAuthType(String str, byte identifier) { + this.str = str; + this.identifier = identifier; + } + + /** + * Find the {@link WellKnownAuthType} for the given identifier (as an {@code int}). Valid + * identifiers are defined to be integers between 0 and 127, inclusive. Identifiers outside of + * this range will produce the {@link #UNPARSEABLE_AUTH_TYPE}. Additionally, some identifiers in + * that range are still only reserved and don't have a type associated yet: this method returns + * the {@link #UNKNOWN_RESERVED_AUTH_TYPE} when passing such an identifier, which lets call sites + * potentially detect this and keep the original representation when transmitting the associated + * metadata buffer. + * + * @param id the looked up identifier + * @return the {@link WellKnownAuthType}, or {@link #UNKNOWN_RESERVED_AUTH_TYPE} if the id is out + * of the specification's range, or {@link #UNKNOWN_RESERVED_AUTH_TYPE} if the id is one that + * is merely reserved but unknown to this implementation. + */ + public static WellKnownAuthType fromIdentifier(int id) { + if (id < 0x00 || id > 0x7F) { + return UNPARSEABLE_AUTH_TYPE; + } + return TYPES_BY_AUTH_ID[id]; + } + + /** + * Find the {@link WellKnownAuthType} for the given {@link String} representation. If the + * representation is {@code null} or doesn't match a {@link WellKnownAuthType}, the {@link + * #UNPARSEABLE_AUTH_TYPE} is returned. + * + * @param authType the looked up auth type + * @return the matching {@link WellKnownAuthType}, or {@link #UNPARSEABLE_AUTH_TYPE} if none + * matches + */ + public static WellKnownAuthType fromString(String authType) { + if (authType == null) throw new IllegalArgumentException("type must be non-null"); + + // force UNPARSEABLE if by chance UNKNOWN_RESERVED_AUTH_TYPE's text has been used + if (authType.equals(UNKNOWN_RESERVED_AUTH_TYPE.str)) { + return UNPARSEABLE_AUTH_TYPE; + } + + return TYPES_BY_AUTH_STRING.getOrDefault(authType, UNPARSEABLE_AUTH_TYPE); + } + + /** @return the byte identifier of the auth type, guaranteed to be positive or zero. */ + public byte getIdentifier() { + return identifier; + } + + /** + * @return the auth type represented as a {@link String}, which is made of US_ASCII compatible + * characters only + */ + public String getString() { + return str; + } + + /** @see #getString() */ + @Override + public String toString() { + return str; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java index 27bf4d1da..fd990d273 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java @@ -2,20 +2,14 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.ByteBufUtil; import io.netty.buffer.Unpooled; -import io.netty.util.CharsetUtil; -import io.rsocket.util.CharByteBufUtil; +import io.rsocket.metadata.AuthMetadataCodec; +/** @deprecated in favor of {@link io.rsocket.metadata.AuthMetadataCodec} */ +@Deprecated public class AuthMetadataFlyweight { static final int STREAM_METADATA_KNOWN_MASK = 0x80; // 1000 0000 - static final byte STREAM_METADATA_LENGTH_MASK = 0x7F; // 0111 1111 - - static final int USERNAME_BYTES_LENGTH = 1; - static final int AUTH_TYPE_ID_LENGTH = 1; - - static final char[] EMPTY_CHARS_ARRAY = new char[0]; private AuthMetadataFlyweight() {} @@ -31,24 +25,7 @@ private AuthMetadataFlyweight() {} public static ByteBuf encodeMetadata( ByteBufAllocator allocator, String customAuthType, ByteBuf metadata) { - int actualASCIILength = ByteBufUtil.utf8Bytes(customAuthType); - if (actualASCIILength != customAuthType.length()) { - throw new IllegalArgumentException("custom auth type must be US_ASCII characters only"); - } - if (actualASCIILength < 1 || actualASCIILength > 128) { - throw new IllegalArgumentException( - "custom auth type must have a strictly positive length that fits on 7 unsigned bits, ie 1-128"); - } - - int capacity = 1 + actualASCIILength; - ByteBuf headerBuffer = allocator.buffer(capacity, capacity); - // encoded length is one less than actual length, since 0 is never a valid length, which gives - // wider representation range - headerBuffer.writeByte(actualASCIILength - 1); - - ByteBufUtil.reserveAndWriteUtf8(headerBuffer, customAuthType, actualASCIILength); - - return allocator.compositeBuffer(2).addComponents(true, headerBuffer, metadata); + return AuthMetadataCodec.encodeMetadata(allocator, customAuthType, metadata); } /** @@ -64,18 +41,7 @@ public static ByteBuf encodeMetadata( public static ByteBuf encodeMetadata( ByteBufAllocator allocator, WellKnownAuthType authType, ByteBuf metadata) { - if (authType == WellKnownAuthType.UNPARSEABLE_AUTH_TYPE - || authType == WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE) { - throw new IllegalArgumentException("only allowed AuthType should be used"); - } - - int capacity = AUTH_TYPE_ID_LENGTH; - ByteBuf headerBuffer = - allocator - .buffer(capacity, capacity) - .writeByte(authType.getIdentifier() | STREAM_METADATA_KNOWN_MASK); - - return allocator.compositeBuffer(2).addComponents(true, headerBuffer, metadata); + return AuthMetadataCodec.encodeMetadata(allocator, WellKnownAuthType.cast(authType), metadata); } /** @@ -88,25 +54,7 @@ public static ByteBuf encodeMetadata( */ public static ByteBuf encodeSimpleMetadata( ByteBufAllocator allocator, char[] username, char[] password) { - - int usernameLength = CharByteBufUtil.utf8Bytes(username); - if (usernameLength > 255) { - throw new IllegalArgumentException( - "Username should be shorter than or equal to 255 bytes length in UTF-8 encoding"); - } - - int passwordLength = CharByteBufUtil.utf8Bytes(password); - int capacity = AUTH_TYPE_ID_LENGTH + USERNAME_BYTES_LENGTH + usernameLength + passwordLength; - final ByteBuf buffer = - allocator - .buffer(capacity, capacity) - .writeByte(WellKnownAuthType.SIMPLE.getIdentifier() | STREAM_METADATA_KNOWN_MASK) - .writeByte(usernameLength); - - CharByteBufUtil.writeUtf8(buffer, username); - CharByteBufUtil.writeUtf8(buffer, password); - - return buffer; + return AuthMetadataCodec.encodeSimpleMetadata(allocator, username, password); } /** @@ -116,17 +64,7 @@ public static ByteBuf encodeSimpleMetadata( * @param token the char sequence which represents BEARER token. */ public static ByteBuf encodeBearerMetadata(ByteBufAllocator allocator, char[] token) { - - int tokenLength = CharByteBufUtil.utf8Bytes(token); - int capacity = AUTH_TYPE_ID_LENGTH + tokenLength; - final ByteBuf buffer = - allocator - .buffer(capacity, capacity) - .writeByte(WellKnownAuthType.BEARER.getIdentifier() | STREAM_METADATA_KNOWN_MASK); - - CharByteBufUtil.writeUtf8(buffer, token); - - return buffer; + return AuthMetadataCodec.encodeBearerMetadata(allocator, token); } /** @@ -146,12 +84,7 @@ public static ByteBuf encodeBearerMetadata(ByteBufAllocator allocator, char[] to */ public static ByteBuf encodeMetadataWithCompression( ByteBufAllocator allocator, String authType, ByteBuf metadata) { - WellKnownAuthType wkn = WellKnownAuthType.fromString(authType); - if (wkn == WellKnownAuthType.UNPARSEABLE_AUTH_TYPE) { - return AuthMetadataFlyweight.encodeMetadata(allocator, authType, metadata); - } else { - return AuthMetadataFlyweight.encodeMetadata(allocator, wkn, metadata); - } + return AuthMetadataCodec.encodeMetadataWithCompression(allocator, authType, metadata); } /** @@ -161,8 +94,7 @@ public static ByteBuf encodeMetadataWithCompression( * @param metadata byteBuf used to get information from */ public static boolean isWellKnownAuthType(ByteBuf metadata) { - byte lengthOrId = metadata.getByte(0); - return (lengthOrId & STREAM_METADATA_LENGTH_MASK) != lengthOrId; + return AuthMetadataCodec.isWellKnownAuthType(metadata); } /** @@ -175,18 +107,7 @@ public static boolean isWellKnownAuthType(ByteBuf metadata) { * @throws IllegalStateException if not enough readable bytes in the given {@link ByteBuf} */ public static WellKnownAuthType decodeWellKnownAuthType(ByteBuf metadata) { - if (metadata.readableBytes() < 1) { - throw new IllegalStateException( - "Unable to decode Well Know Auth type. Not enough readable bytes"); - } - byte lengthOrId = metadata.readByte(); - int normalizedId = (byte) (lengthOrId & STREAM_METADATA_LENGTH_MASK); - - if (normalizedId != lengthOrId) { - return WellKnownAuthType.fromIdentifier(normalizedId); - } - - return WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; + return WellKnownAuthType.cast(AuthMetadataCodec.decodeWellKnownAuthType(metadata)); } /** @@ -196,25 +117,7 @@ public static WellKnownAuthType decodeWellKnownAuthType(ByteBuf metadata) { * @return */ public static CharSequence decodeCustomAuthType(ByteBuf metadata) { - if (metadata.readableBytes() < 2) { - throw new IllegalStateException( - "Unable to decode custom Auth type. Not enough readable bytes"); - } - - byte encodedLength = metadata.readByte(); - if (encodedLength < 0) { - throw new IllegalStateException( - "Unable to decode custom Auth type. Incorrect auth type length"); - } - - // encoded length is realLength - 1 in order to avoid intersection with 0x00 authtype - int realLength = encodedLength + 1; - if (metadata.readableBytes() < realLength) { - throw new IllegalArgumentException( - "Unable to decode custom Auth type. Malformed length or auth type string"); - } - - return metadata.readCharSequence(realLength, CharsetUtil.US_ASCII); + return AuthMetadataCodec.decodeCustomAuthType(metadata); } /** @@ -227,11 +130,7 @@ public static CharSequence decodeCustomAuthType(ByteBuf metadata) { * given one */ public static ByteBuf decodePayload(ByteBuf metadata) { - if (metadata.readableBytes() == 0) { - return Unpooled.EMPTY_BUFFER; - } - - return metadata.readSlice(metadata.readableBytes()); + return AuthMetadataCodec.decodePayload(metadata); } /** @@ -243,13 +142,7 @@ public static ByteBuf decodePayload(ByteBuf metadata) { * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if username length is zero */ public static ByteBuf decodeUsername(ByteBuf simpleAuthMetadata) { - short usernameLength = decodeUsernameLength(simpleAuthMetadata); - - if (usernameLength == 0) { - return Unpooled.EMPTY_BUFFER; - } - - return simpleAuthMetadata.readSlice(usernameLength); + return AuthMetadataCodec.decodeUsername(simpleAuthMetadata); } /** @@ -261,11 +154,7 @@ public static ByteBuf decodeUsername(ByteBuf simpleAuthMetadata) { * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if password length is zero */ public static ByteBuf decodePassword(ByteBuf simpleAuthMetadata) { - if (simpleAuthMetadata.readableBytes() == 0) { - return Unpooled.EMPTY_BUFFER; - } - - return simpleAuthMetadata.readSlice(simpleAuthMetadata.readableBytes()); + return AuthMetadataCodec.decodePassword(simpleAuthMetadata); } /** * Read up to 257 {@code bytes} from the given {@link ByteBuf} where the first byte is username @@ -276,13 +165,7 @@ public static ByteBuf decodePassword(ByteBuf simpleAuthMetadata) { * @return {@code char[]} which represents UTF-8 username */ public static char[] decodeUsernameAsCharArray(ByteBuf simpleAuthMetadata) { - short usernameLength = decodeUsernameLength(simpleAuthMetadata); - - if (usernameLength == 0) { - return EMPTY_CHARS_ARRAY; - } - - return CharByteBufUtil.readUtf8(simpleAuthMetadata, usernameLength); + return AuthMetadataCodec.decodeUsernameAsCharArray(simpleAuthMetadata); } /** @@ -294,11 +177,7 @@ public static char[] decodeUsernameAsCharArray(ByteBuf simpleAuthMetadata) { * @return {@code char[]} which represents UTF-8 password */ public static char[] decodePasswordAsCharArray(ByteBuf simpleAuthMetadata) { - if (simpleAuthMetadata.readableBytes() == 0) { - return EMPTY_CHARS_ARRAY; - } - - return CharByteBufUtil.readUtf8(simpleAuthMetadata, simpleAuthMetadata.readableBytes()); + return AuthMetadataCodec.decodePasswordAsCharArray(simpleAuthMetadata); } /** @@ -310,26 +189,6 @@ public static char[] decodePasswordAsCharArray(ByteBuf simpleAuthMetadata) { * @return {@code char[]} which represents UTF-8 password */ public static char[] decodeBearerTokenAsCharArray(ByteBuf bearerAuthMetadata) { - if (bearerAuthMetadata.readableBytes() == 0) { - return EMPTY_CHARS_ARRAY; - } - - return CharByteBufUtil.readUtf8(bearerAuthMetadata, bearerAuthMetadata.readableBytes()); - } - - private static short decodeUsernameLength(ByteBuf simpleAuthMetadata) { - if (simpleAuthMetadata.readableBytes() < 1) { - throw new IllegalStateException( - "Unable to decode custom username. Not enough readable bytes"); - } - - short usernameLength = simpleAuthMetadata.readUnsignedByte(); - - if (simpleAuthMetadata.readableBytes() < usernameLength) { - throw new IllegalArgumentException( - "Unable to decode username. Malformed username length or content"); - } - - return usernameLength; + return AuthMetadataCodec.decodeBearerTokenAsCharArray(bearerAuthMetadata); } } diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/security/WellKnownAuthType.java b/rsocket-core/src/main/java/io/rsocket/metadata/security/WellKnownAuthType.java index bd4b656b8..24e5ff0db 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/security/WellKnownAuthType.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/security/WellKnownAuthType.java @@ -24,7 +24,10 @@ * Enumeration of Well Known Auth Types, as defined in the eponymous extension. Such auth types are * used in composite metadata (which can include routing and/or tracing metadata). Per * specification, identifiers are between 0 and 127 (inclusive). + * + * @deprecated in favor of {@link io.rsocket.metadata.WellKnownAuthType} */ +@Deprecated public enum WellKnownAuthType { UNPARSEABLE_AUTH_TYPE("UNPARSEABLE_AUTH_TYPE_DO_NOT_USE", (byte) -2), UNKNOWN_RESERVED_AUTH_TYPE("UNKNOWN_YET_RESERVED_DO_NOT_USE", (byte) -1), @@ -59,6 +62,29 @@ public enum WellKnownAuthType { this.identifier = identifier; } + static io.rsocket.metadata.WellKnownAuthType cast(WellKnownAuthType wellKnownAuthType) { + byte identifier = wellKnownAuthType.identifier; + if (identifier == io.rsocket.metadata.WellKnownAuthType.UNPARSEABLE_AUTH_TYPE.getIdentifier()) { + return io.rsocket.metadata.WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; + } else if (identifier + == io.rsocket.metadata.WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE.getIdentifier()) { + return io.rsocket.metadata.WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE; + } else { + return io.rsocket.metadata.WellKnownAuthType.fromIdentifier(identifier); + } + } + + static WellKnownAuthType cast(io.rsocket.metadata.WellKnownAuthType wellKnownAuthType) { + byte identifier = wellKnownAuthType.getIdentifier(); + if (identifier == WellKnownAuthType.UNPARSEABLE_AUTH_TYPE.identifier) { + return WellKnownAuthType.UNPARSEABLE_AUTH_TYPE; + } else if (identifier == WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE.identifier) { + return WellKnownAuthType.UNKNOWN_RESERVED_AUTH_TYPE; + } else { + return TYPES_BY_AUTH_ID[identifier]; + } + } + /** * Find the {@link WellKnownAuthType} for the given identifier (as an {@code int}). Valid * identifiers are defined to be integers between 0 and 127, inclusive. Identifiers outside of diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java index 01b6dfeae..ed9450357 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ClientRSocketSession.java @@ -20,9 +20,9 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.exceptions.ConnectionErrorException; -import io.rsocket.frame.ErrorFrameFlyweight; -import io.rsocket.frame.ResumeFrameFlyweight; -import io.rsocket.frame.ResumeOkFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.ResumeFrameCodec; +import io.rsocket.frame.ResumeOkFrameCodec; import io.rsocket.internal.ClientServerInputMultiplexer; import java.time.Duration; import java.util.concurrent.atomic.AtomicBoolean; @@ -86,7 +86,7 @@ public ClientRSocketSession( position); /*Connection is established again: send RESUME frame to server, listen for RESUME_OK*/ sendFrame( - ResumeFrameFlyweight.encode( + ResumeFrameCodec.encode( allocator, /*retain so token is not released once sent as part of resume frame*/ resumeToken.retain(), @@ -123,7 +123,7 @@ public ClientRSocketSession resumeWith(ByteBuf resumeOkFrame) { .onErrorResume( err -> sendFrame( - ErrorFrameFlyweight.encode( + ErrorFrameCodec.encode( allocator, 0, errorFrameThrowable(remoteImpliedPos))) .then(Mono.fromRunnable(resumableConnection::dispose)) /*Resumption is impossible: no need to return control to ResumableConnection*/ @@ -157,7 +157,7 @@ private Mono sendFrame(ByteBuf frame) { } private static long remoteImpliedPos(ByteBuf resumeOkFrame) { - return ResumeOkFrameFlyweight.lastReceivedClientPos(resumeOkFrame); + return ResumeOkFrameCodec.lastReceivedClientPos(resumeOkFrame); } private static long remotePos(ByteBuf resumeOkFrame) { 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 980de2de1..461d71228 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ResumableDuplexConnection.java @@ -20,7 +20,7 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.Closeable; import io.rsocket.DuplexConnection; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameHeaderCodec; import java.nio.channels.ClosedChannelException; import java.time.Duration; import java.util.Queue; @@ -373,7 +373,7 @@ private void releaseFramesToPosition(long remoteImpliedPos) { } static boolean isResumableFrame(ByteBuf frame) { - switch (FrameHeaderFlyweight.nativeFrameType(frame)) { + switch (FrameHeaderCodec.nativeFrameType(frame)) { case REQUEST_CHANNEL: case REQUEST_STREAM: case REQUEST_RESPONSE: diff --git a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java index 5d55559cc..b54ce644f 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/ServerRSocketSession.java @@ -20,9 +20,9 @@ import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; import io.rsocket.exceptions.RejectedResumeException; -import io.rsocket.frame.ErrorFrameFlyweight; -import io.rsocket.frame.ResumeFrameFlyweight; -import io.rsocket.frame.ResumeOkFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.ResumeFrameCodec; +import io.rsocket.frame.ResumeOkFrameCodec; import java.time.Duration; import java.util.function.Function; import org.slf4j.Logger; @@ -103,12 +103,10 @@ public ServerRSocketSession resumeWith(ByteBuf resumeFrame) { remotePos, remoteImpliedPos, pos -> - pos.flatMap( - impliedPos -> sendFrame(ResumeOkFrameFlyweight.encode(allocator, impliedPos))) + pos.flatMap(impliedPos -> sendFrame(ResumeOkFrameCodec.encode(allocator, impliedPos))) .onErrorResume( err -> - sendFrame( - ErrorFrameFlyweight.encode(allocator, 0, errorFrameThrowable(err))) + sendFrame(ErrorFrameCodec.encode(allocator, 0, errorFrameThrowable(err))) .then(Mono.fromRunnable(resumableConnection::dispose)) /*Resumption is impossible: no need to return control to ResumableConnection*/ .then(Mono.never()))); @@ -136,11 +134,11 @@ private Mono sendFrame(ByteBuf frame) { } private static long remotePos(ByteBuf resumeFrame) { - return ResumeFrameFlyweight.firstAvailableClientPos(resumeFrame); + return ResumeFrameCodec.firstAvailableClientPos(resumeFrame); } private static long remoteImpliedPos(ByteBuf resumeFrame) { - return ResumeFrameFlyweight.lastReceivedServerPos(resumeFrame); + return ResumeFrameCodec.lastReceivedServerPos(resumeFrame); } private static RejectedResumeException errorFrameThrowable(Throwable err) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java b/rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java index ea3142d25..9d8b8354a 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java @@ -8,7 +8,7 @@ import io.netty.buffer.Unpooled; import io.rsocket.ConnectionSetupPayload; import io.rsocket.Payload; -import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.frame.SetupFrameCodec; import io.rsocket.util.DefaultPayload; import org.junit.jupiter.api.Test; @@ -31,8 +31,8 @@ void testSetupPayloadWithDataMetadata() { assertTrue(setupPayload.willClientHonorLease()); assertEquals(KEEP_ALIVE_INTERVAL, setupPayload.keepAliveInterval()); assertEquals(KEEP_ALIVE_MAX_LIFETIME, setupPayload.keepAliveMaxLifetime()); - assertEquals(METADATA_TYPE, SetupFrameFlyweight.metadataMimeType(frame)); - assertEquals(DATA_TYPE, SetupFrameFlyweight.dataMimeType(frame)); + assertEquals(METADATA_TYPE, SetupFrameCodec.metadataMimeType(frame)); + assertEquals(DATA_TYPE, SetupFrameCodec.dataMimeType(frame)); assertTrue(setupPayload.hasMetadata()); assertNotNull(setupPayload.metadata()); assertEquals(payload.metadata(), setupPayload.metadata()); @@ -77,7 +77,7 @@ void testSetupPayloadWithEmptyMetadata() { } private static ByteBuf encodeSetupFrame(boolean leaseEnabled, Payload setupPayload) { - return SetupFrameFlyweight.encode( + return SetupFrameCodec.encode( ByteBufAllocator.DEFAULT, leaseEnabled, KEEP_ALIVE_INTERVAL, diff --git a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java index 7e465db08..b3ded08ec 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java @@ -26,9 +26,9 @@ import io.rsocket.TestScheduler; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.ConnectionErrorException; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; -import io.rsocket.frame.KeepAliveFrameFlyweight; +import io.rsocket.frame.KeepAliveFrameCodec; import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.resume.InMemoryResumableFramesStore; import io.rsocket.resume.ResumableDuplexConnection; @@ -115,7 +115,7 @@ void rSocketNotDisposedOnPresentKeepAlives() { .subscribe( n -> connection.addToReceivedBuffer( - KeepAliveFrameFlyweight.encode( + KeepAliveFrameCodec.encode( ByteBufAllocator.DEFAULT, true, 0, Unpooled.EMPTY_BUFFER))); Mono.delay(Duration.ofMillis(2000)).block(); @@ -171,7 +171,7 @@ void requesterRespondsToKeepAlives() { .subscribe( l -> connection.addToReceivedBuffer( - KeepAliveFrameFlyweight.encode( + KeepAliveFrameCodec.encode( ByteBufAllocator.DEFAULT, true, 0, Unpooled.EMPTY_BUFFER))); StepVerifier.create(Flux.from(connection.getSentAsPublisher()).take(1)) @@ -235,15 +235,15 @@ void resumableRSocketsNotDisposedOnMissingKeepAlives() { } private boolean keepAliveFrame(ByteBuf frame) { - return FrameHeaderFlyweight.frameType(frame) == FrameType.KEEPALIVE; + return FrameHeaderCodec.frameType(frame) == FrameType.KEEPALIVE; } private boolean keepAliveFrameWithRespondFlag(ByteBuf frame) { - return keepAliveFrame(frame) && KeepAliveFrameFlyweight.respondFlag(frame); + return keepAliveFrame(frame) && KeepAliveFrameCodec.respondFlag(frame); } private boolean keepAliveFrameWithoutRespondFlag(ByteBuf frame) { - return keepAliveFrame(frame) && !KeepAliveFrameFlyweight.respondFlag(frame); + return keepAliveFrame(frame) && !KeepAliveFrameCodec.respondFlag(frame); } static class RSocketState { diff --git a/rsocket-core/src/test/java/io/rsocket/core/PayloadValidationUtilsTest.java b/rsocket-core/src/test/java/io/rsocket/core/PayloadValidationUtilsTest.java index e91fce848..ed9f1ec4a 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/PayloadValidationUtilsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/PayloadValidationUtilsTest.java @@ -1,8 +1,8 @@ package io.rsocket.core; import io.rsocket.Payload; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameLengthCodec; import io.rsocket.util.DefaultPayload; import java.util.concurrent.ThreadLocalRandom; import org.assertj.core.api.Assertions; @@ -14,9 +14,9 @@ class PayloadValidationUtilsTest { void shouldBeValidFrameWithNoFragmentation() { byte[] data = new byte - [FrameLengthFlyweight.FRAME_LENGTH_MASK - - FrameLengthFlyweight.FRAME_LENGTH_SIZE - - FrameHeaderFlyweight.size()]; + [FrameLengthCodec.FRAME_LENGTH_MASK + - FrameLengthCodec.FRAME_LENGTH_SIZE + - FrameHeaderCodec.size()]; ThreadLocalRandom.current().nextBytes(data); final Payload payload = DefaultPayload.create(data); @@ -27,9 +27,9 @@ void shouldBeValidFrameWithNoFragmentation() { void shouldBeInValidFrameWithNoFragmentation() { byte[] data = new byte - [FrameLengthFlyweight.FRAME_LENGTH_MASK - - FrameLengthFlyweight.FRAME_LENGTH_SIZE - - FrameHeaderFlyweight.size() + [FrameLengthCodec.FRAME_LENGTH_MASK + - FrameLengthCodec.FRAME_LENGTH_SIZE + - FrameHeaderCodec.size() + 1]; ThreadLocalRandom.current().nextBytes(data); final Payload payload = DefaultPayload.create(data); @@ -39,13 +39,13 @@ void shouldBeInValidFrameWithNoFragmentation() { @Test void shouldBeValidFrameWithNoFragmentation0() { - byte[] metadata = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK / 2]; + byte[] metadata = new byte[FrameLengthCodec.FRAME_LENGTH_MASK / 2]; byte[] data = new byte - [FrameLengthFlyweight.FRAME_LENGTH_MASK / 2 - - FrameLengthFlyweight.FRAME_LENGTH_SIZE - - FrameHeaderFlyweight.size() - - FrameHeaderFlyweight.size()]; + [FrameLengthCodec.FRAME_LENGTH_MASK / 2 + - FrameLengthCodec.FRAME_LENGTH_SIZE + - FrameHeaderCodec.size() + - FrameHeaderCodec.size()]; ThreadLocalRandom.current().nextBytes(data); ThreadLocalRandom.current().nextBytes(metadata); final Payload payload = DefaultPayload.create(data, metadata); @@ -55,8 +55,8 @@ void shouldBeValidFrameWithNoFragmentation0() { @Test void shouldBeInValidFrameWithNoFragmentation1() { - byte[] metadata = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; - byte[] data = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + byte[] metadata = new byte[FrameLengthCodec.FRAME_LENGTH_MASK]; + byte[] data = new byte[FrameLengthCodec.FRAME_LENGTH_MASK]; ThreadLocalRandom.current().nextBytes(metadata); ThreadLocalRandom.current().nextBytes(data); final Payload payload = DefaultPayload.create(data, metadata); @@ -77,8 +77,8 @@ void shouldBeValidFrameWithNoFragmentation2() { @Test void shouldBeValidFrameWithNoFragmentation3() { - byte[] metadata = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; - byte[] data = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + byte[] metadata = new byte[FrameLengthCodec.FRAME_LENGTH_MASK]; + byte[] data = new byte[FrameLengthCodec.FRAME_LENGTH_MASK]; ThreadLocalRandom.current().nextBytes(metadata); ThreadLocalRandom.current().nextBytes(data); final Payload payload = DefaultPayload.create(data, metadata); diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java index 04d5fe174..ddfbe4234 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -31,10 +31,10 @@ import io.rsocket.TestScheduler; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.Exceptions; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; -import io.rsocket.frame.LeaseFrameFlyweight; -import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.frame.LeaseFrameCodec; +import io.rsocket.frame.SetupFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.ClientServerInputMultiplexer; import io.rsocket.lease.*; @@ -123,7 +123,7 @@ void setUp() { public void serverRSocketFactoryRejectsUnsupportedLease() { Payload payload = DefaultPayload.create(DefaultPayload.EMPTY_BUFFER); ByteBuf setupFrame = - SetupFrameFlyweight.encode( + SetupFrameCodec.encode( ByteBufAllocator.DEFAULT, true, 1000, @@ -141,7 +141,7 @@ public void serverRSocketFactoryRejectsUnsupportedLease() { Collection sent = connection.getSent(); Assertions.assertThat(sent).hasSize(1); ByteBuf error = sent.iterator().next(); - Assertions.assertThat(FrameHeaderFlyweight.frameType(error)).isEqualTo(ERROR); + Assertions.assertThat(FrameHeaderCodec.frameType(error)).isEqualTo(ERROR); Assertions.assertThat(Exceptions.from(0, error).getMessage()) .isEqualTo("lease is not supported"); } @@ -154,8 +154,8 @@ public void clientRSocketFactorySetsLeaseFlag() { Collection sent = clientTransport.testConnection().getSent(); Assertions.assertThat(sent).hasSize(1); ByteBuf setup = sent.iterator().next(); - Assertions.assertThat(FrameHeaderFlyweight.frameType(setup)).isEqualTo(SETUP); - Assertions.assertThat(SetupFrameFlyweight.honorLease(setup)).isTrue(); + Assertions.assertThat(FrameHeaderCodec.frameType(setup)).isEqualTo(SETUP); + Assertions.assertThat(SetupFrameCodec.honorLease(setup)).isTrue(); } @ParameterizedTest @@ -278,13 +278,13 @@ void sendLease() { connection .getSent() .stream() - .filter(f -> FrameHeaderFlyweight.frameType(f) == FrameType.LEASE) + .filter(f -> FrameHeaderCodec.frameType(f) == FrameType.LEASE) .findFirst() .orElseThrow(() -> new IllegalStateException("Lease frame not sent")); - Assertions.assertThat(LeaseFrameFlyweight.ttl(leaseFrame)).isEqualTo(ttl); - Assertions.assertThat(LeaseFrameFlyweight.numRequests(leaseFrame)).isEqualTo(numberOfRequests); - Assertions.assertThat(LeaseFrameFlyweight.metadata(leaseFrame).toString(utf8)) + Assertions.assertThat(LeaseFrameCodec.ttl(leaseFrame)).isEqualTo(ttl); + Assertions.assertThat(LeaseFrameCodec.numRequests(leaseFrame)).isEqualTo(numberOfRequests); + Assertions.assertThat(LeaseFrameCodec.metadata(leaseFrame).toString(utf8)) .isEqualTo(metadataContent); } @@ -312,7 +312,7 @@ void receiveLease() { } ByteBuf leaseFrame(int ttl, int requests, ByteBuf metadata) { - return LeaseFrameFlyweight.encode(byteBufAllocator, ttl, requests, metadata); + return LeaseFrameCodec.encode(byteBufAllocator, ttl, requests, metadata); } static Stream>> interactions() { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index 3e7479af3..6f1ecf98b 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -21,9 +21,9 @@ import io.rsocket.RSocket; import io.rsocket.TestScheduler; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; -import io.rsocket.frame.PayloadFrameFlyweight; +import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.subscriber.AssertSubscriber; import io.rsocket.lease.RequesterLeaseHandler; @@ -87,7 +87,7 @@ void singleSubscriber(Function> interaction) { response.subscribe(assertSubscriberA); response.subscribe(assertSubscriberB); - connection.addToReceivedBuffer(PayloadFrameFlyweight.encodeComplete(connection.alloc(), 1)); + connection.addToReceivedBuffer(PayloadFrameCodec.encodeComplete(connection.alloc(), 1)); assertSubscriberA.assertTerminated(); assertSubscriberB.assertTerminated(); @@ -106,7 +106,7 @@ void singleSubscriberInCaseOfRacing(Function> interaction) RaceTestUtils.race( () -> response.subscribe(assertSubscriberA), () -> response.subscribe(assertSubscriberB)); - connection.addToReceivedBuffer(PayloadFrameFlyweight.encodeComplete(connection.alloc(), i)); + connection.addToReceivedBuffer(PayloadFrameCodec.encodeComplete(connection.alloc(), i)); assertSubscriberA.assertTerminated(); assertSubscriberB.assertTerminated(); @@ -117,7 +117,7 @@ void singleSubscriberInCaseOfRacing(Function> interaction) Assertions.assertThat(connection.getSent()) .hasSize(1) .first() - .matches(bb -> REQUEST_TYPES.contains(FrameHeaderFlyweight.frameType(bb))); + .matches(bb -> REQUEST_TYPES.contains(FrameHeaderCodec.frameType(bb))); connection.clearSendReceiveBuffers(); } } @@ -133,7 +133,7 @@ void singleSubscriberInteractionsAreLazy(Function> interac static long requestFramesCount(Collection frames) { return frames .stream() - .filter(frame -> REQUEST_TYPES.contains(FrameHeaderFlyweight.frameType(frame))) + .filter(frame -> REQUEST_TYPES.contains(FrameHeaderCodec.frameType(frame))) .count(); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index 56e6c9342..e4943e9b0 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -17,7 +17,7 @@ package io.rsocket.core; import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; -import static io.rsocket.frame.FrameHeaderFlyweight.frameType; +import static io.rsocket.frame.FrameHeaderCodec.frameType; import static io.rsocket.frame.FrameType.CANCEL; import static io.rsocket.frame.FrameType.REQUEST_CHANNEL; import static io.rsocket.frame.FrameType.REQUEST_FNF; @@ -46,17 +46,17 @@ import io.rsocket.exceptions.ApplicationErrorException; import io.rsocket.exceptions.CustomRSocketException; import io.rsocket.exceptions.RejectedSetupException; -import io.rsocket.frame.CancelFrameFlyweight; -import io.rsocket.frame.ErrorFrameFlyweight; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.frame.CancelFrameCodec; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameLengthCodec; import io.rsocket.frame.FrameType; -import io.rsocket.frame.PayloadFrameFlyweight; -import io.rsocket.frame.RequestChannelFrameFlyweight; -import io.rsocket.frame.RequestFireAndForgetFrameFlyweight; -import io.rsocket.frame.RequestNFrameFlyweight; -import io.rsocket.frame.RequestResponseFrameFlyweight; -import io.rsocket.frame.RequestStreamFrameFlyweight; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestNFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.subscriber.AssertSubscriber; import io.rsocket.lease.RequesterLeaseHandler; @@ -123,7 +123,7 @@ public void tearDown() { @Test @Timeout(2_000) public void testInvalidFrameOnStream0() { - rule.connection.addToReceivedBuffer(RequestNFrameFlyweight.encode(rule.alloc(), 0, 10)); + rule.connection.addToReceivedBuffer(RequestNFrameCodec.encode(rule.alloc(), 0, 10)); assertThat("Unexpected errors.", rule.errors, hasSize(1)); assertThat( "Unexpected error received.", @@ -157,7 +157,7 @@ protected void hookOnSubscribe(Subscription subscription) { ByteBuf f = sent.get(0); assertThat("initial frame", frameType(f), is(REQUEST_STREAM)); - assertThat("initial request n", RequestStreamFrameFlyweight.initialRequestN(f), is(5L)); + assertThat("initial request n", RequestStreamFrameCodec.initialRequestN(f), is(5L)); assertThat("should be released", f.release(), is(true)); rule.assertHasNoLeaks(); } @@ -166,7 +166,7 @@ protected void hookOnSubscribe(Subscription subscription) { @Timeout(2_000) public void testHandleSetupException() { rule.connection.addToReceivedBuffer( - ErrorFrameFlyweight.encode(rule.alloc(), 0, new RejectedSetupException("boom"))); + ErrorFrameCodec.encode(rule.alloc(), 0, new RejectedSetupException("boom"))); assertThat("Unexpected errors.", rule.errors, hasSize(1)); assertThat( "Unexpected error received.", @@ -185,7 +185,7 @@ public void testHandleApplicationException() { int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); rule.connection.addToReceivedBuffer( - ErrorFrameFlyweight.encode(rule.alloc(), streamId, new ApplicationErrorException("error"))); + ErrorFrameCodec.encode(rule.alloc(), streamId, new ApplicationErrorException("error"))); verify(responseSub).onError(any(ApplicationErrorException.class)); @@ -206,7 +206,7 @@ public void testHandleValidFrame() { int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); rule.connection.addToReceivedBuffer( - PayloadFrameFlyweight.encodeNextReleasingPayload( + PayloadFrameCodec.encodeNextReleasingPayload( rule.alloc(), streamId, EmptyPayload.INSTANCE)); verify(sub).onComplete(); @@ -288,9 +288,8 @@ public void testChannelRequestServerSideCancellation() { request.onNext(EmptyPayload.INSTANCE); rule.socket.requestChannel(request).subscribe(cancelled); int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); - rule.connection.addToReceivedBuffer(CancelFrameFlyweight.encode(rule.alloc(), streamId)); - rule.connection.addToReceivedBuffer( - PayloadFrameFlyweight.encodeComplete(rule.alloc(), streamId)); + rule.connection.addToReceivedBuffer(CancelFrameCodec.encode(rule.alloc(), streamId)); + rule.connection.addToReceivedBuffer(PayloadFrameCodec.encodeComplete(rule.alloc(), streamId)); Flux.first( cancelled, Flux.error(new IllegalStateException("Channel request not cancelled")) @@ -328,11 +327,10 @@ protected void hookOnSubscribe(Subscription subscription) {} ByteBuf initialFrame = iterator.next(); - Assertions.assertThat(FrameHeaderFlyweight.frameType(initialFrame)).isEqualTo(REQUEST_CHANNEL); - Assertions.assertThat(RequestChannelFrameFlyweight.initialRequestN(initialFrame)) + Assertions.assertThat(FrameHeaderCodec.frameType(initialFrame)).isEqualTo(REQUEST_CHANNEL); + Assertions.assertThat(RequestChannelFrameCodec.initialRequestN(initialFrame)) .isEqualTo(Long.MAX_VALUE); - Assertions.assertThat( - RequestChannelFrameFlyweight.data(initialFrame).toString(CharsetUtil.UTF_8)) + Assertions.assertThat(RequestChannelFrameCodec.data(initialFrame).toString(CharsetUtil.UTF_8)) .isEqualTo("0"); Assertions.assertThat(initialFrame.release()).isTrue(); @@ -345,8 +343,8 @@ public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmen prepareCalls() .forEach( generator -> { - byte[] metadata = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; - byte[] data = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + byte[] metadata = new byte[FrameLengthCodec.FRAME_LENGTH_MASK]; + byte[] data = new byte[FrameLengthCodec.FRAME_LENGTH_MASK]; ThreadLocalRandom.current().nextBytes(metadata); ThreadLocalRandom.current().nextBytes(data); StepVerifier.create( @@ -374,8 +372,8 @@ static Stream>> prepareCalls() { @Test public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmentationForRequestChannelCase() { - byte[] metadata = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; - byte[] data = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + byte[] metadata = new byte[FrameLengthCodec.FRAME_LENGTH_MASK]; + byte[] data = new byte[FrameLengthCodec.FRAME_LENGTH_MASK]; ThreadLocalRandom.current().nextBytes(metadata); ThreadLocalRandom.current().nextBytes(data); StepVerifier.create( @@ -385,7 +383,7 @@ static Stream>> prepareCalls() { .then( () -> rule.connection.addToReceivedBuffer( - RequestNFrameFlyweight.encode( + RequestNFrameCodec.encode( rule.alloc(), rule.getStreamIdForRequestType(REQUEST_CHANNEL), 2))) .expectErrorSatisfies( t -> @@ -454,7 +452,7 @@ private static Stream racingCases() { as.request(1); int streamId = rule.getStreamIdForRequestType(REQUEST_STREAM); ByteBuf frame = - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, streamId, false, false, true, metadata, data); RaceTestUtils.race(as::cancel, () -> rule.connection.addToReceivedBuffer(frame)); @@ -472,7 +470,7 @@ private static Stream racingCases() { as.request(1); int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); ByteBuf frame = - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, streamId, false, false, true, metadata, data); RaceTestUtils.race(as::cancel, () -> rule.connection.addToReceivedBuffer(frame)); @@ -582,7 +580,7 @@ private static Stream racingCases() { ByteBufAllocator allocator = rule.alloc(); as.request(1); int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); - ByteBuf frame = CancelFrameFlyweight.encode(allocator, streamId); + ByteBuf frame = CancelFrameCodec.encode(allocator, streamId); RaceTestUtils.race( () -> as.request(Long.MAX_VALUE), @@ -609,7 +607,7 @@ private static Stream racingCases() { as.request(1); int streamId = rule.getStreamIdForRequestType(REQUEST_CHANNEL); ByteBuf frame = - ErrorFrameFlyweight.encode(allocator, streamId, new RuntimeException("test")); + ErrorFrameCodec.encode(allocator, streamId, new RuntimeException("test")); RaceTestUtils.race( () -> as.request(Long.MAX_VALUE), @@ -628,7 +626,7 @@ private static Stream racingCases() { as.request(Long.MAX_VALUE); int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); ByteBuf frame = - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, streamId, false, false, true, metadata, data); RaceTestUtils.race(as::cancel, () -> rule.connection.addToReceivedBuffer(frame)); @@ -672,7 +670,7 @@ public void simpleOnDiscardRequestChannelTest2() { testPublisher.next(ByteBufPayload.create("d1", "m1"), ByteBufPayload.create("d2", "m2")); rule.connection.addToReceivedBuffer( - ErrorFrameFlyweight.encode( + ErrorFrameCodec.encode( allocator, streamId, new CustomRSocketException(0x00000404, "test"))); Assertions.assertThat(rule.connection.getSent()).allMatch(ByteBuf::release); @@ -716,7 +714,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( if (responsesCnt > 0) { for (int i = 0; i < responsesCnt - 1; i++) { rule.connection.addToReceivedBuffer( - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, streamId, false, @@ -727,7 +725,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( } rule.connection.addToReceivedBuffer( - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, streamId, false, @@ -739,7 +737,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( if (framesCnt > 1) { rule.connection.addToReceivedBuffer( - RequestNFrameFlyweight.encode(allocator, streamId, framesCnt)); + RequestNFrameCodec.encode(allocator, streamId, framesCnt)); } for (int i = 1; i < framesCnt; i++) { @@ -750,7 +748,7 @@ public void verifiesThatFrameWithNoMetadataHasDecodedCorrectlyIntoPayload( .describedAs( "Interaction Type :[%s]. Expected to observe %s frames sent", frameType, framesCnt) .hasSize(framesCnt) - .allMatch(bb -> !FrameHeaderFlyweight.hasMetadata(bb)) + .allMatch(bb -> !FrameHeaderCodec.hasMetadata(bb)) .allMatch(ByteBuf::release); Assertions.assertThat(assertSubscriber.isTerminated()) @@ -818,12 +816,12 @@ public void ensuresThatNoOpsMustHappenUntilSubscriptionInCaseOfFnfCall() { .hasSize(1) .first() .matches(bb -> frameType(bb) == REQUEST_FNF) - .matches(bb -> FrameHeaderFlyweight.streamId(bb) == 1) + .matches(bb -> FrameHeaderCodec.streamId(bb) == 1) // ensures that this is fnf1 with abc2 data .matches( bb -> ByteBufUtil.equals( - RequestFireAndForgetFrameFlyweight.data(bb), + RequestFireAndForgetFrameCodec.data(bb), Unpooled.wrappedBuffer("abc2".getBytes()))) .matches(ReferenceCounted::release); @@ -836,12 +834,12 @@ public void ensuresThatNoOpsMustHappenUntilSubscriptionInCaseOfFnfCall() { .hasSize(1) .first() .matches(bb -> frameType(bb) == REQUEST_FNF) - .matches(bb -> FrameHeaderFlyweight.streamId(bb) == 3) + .matches(bb -> FrameHeaderCodec.streamId(bb) == 3) // ensures that this is fnf1 with abc1 data .matches( bb -> ByteBufUtil.equals( - RequestFireAndForgetFrameFlyweight.data(bb), + RequestFireAndForgetFrameCodec.data(bb), Unpooled.wrappedBuffer("abc1".getBytes()))) .matches(ReferenceCounted::release); } @@ -875,25 +873,23 @@ public void ensuresThatNoOpsMustHappenUntilFirstRequestN( .first() .matches(bb -> frameType(bb) == frameType) .matches( - bb -> FrameHeaderFlyweight.streamId(bb) == 1, + bb -> FrameHeaderCodec.streamId(bb) == 1, "Expected to have stream ID {1} but got {" - + FrameHeaderFlyweight.streamId(rule.connection.getSent().iterator().next()) + + FrameHeaderCodec.streamId(rule.connection.getSent().iterator().next()) + "}") .matches( bb -> { switch (frameType) { case REQUEST_RESPONSE: return ByteBufUtil.equals( - RequestResponseFrameFlyweight.data(bb), + RequestResponseFrameCodec.data(bb), Unpooled.wrappedBuffer("abc2".getBytes())); case REQUEST_STREAM: return ByteBufUtil.equals( - RequestStreamFrameFlyweight.data(bb), - Unpooled.wrappedBuffer("abc2".getBytes())); + RequestStreamFrameCodec.data(bb), Unpooled.wrappedBuffer("abc2".getBytes())); case REQUEST_CHANNEL: return ByteBufUtil.equals( - RequestChannelFrameFlyweight.data(bb), - Unpooled.wrappedBuffer("abc2".getBytes())); + RequestChannelFrameCodec.data(bb), Unpooled.wrappedBuffer("abc2".getBytes())); } return false; @@ -908,25 +904,23 @@ public void ensuresThatNoOpsMustHappenUntilFirstRequestN( .first() .matches(bb -> frameType(bb) == frameType) .matches( - bb -> FrameHeaderFlyweight.streamId(bb) == 3, + bb -> FrameHeaderCodec.streamId(bb) == 3, "Expected to have stream ID {1} but got {" - + FrameHeaderFlyweight.streamId(rule.connection.getSent().iterator().next()) + + FrameHeaderCodec.streamId(rule.connection.getSent().iterator().next()) + "}") .matches( bb -> { switch (frameType) { case REQUEST_RESPONSE: return ByteBufUtil.equals( - RequestResponseFrameFlyweight.data(bb), + RequestResponseFrameCodec.data(bb), Unpooled.wrappedBuffer("abc1".getBytes())); case REQUEST_STREAM: return ByteBufUtil.equals( - RequestStreamFrameFlyweight.data(bb), - Unpooled.wrappedBuffer("abc1".getBytes())); + RequestStreamFrameCodec.data(bb), Unpooled.wrappedBuffer("abc1".getBytes())); case REQUEST_CHANNEL: return ByteBufUtil.equals( - RequestChannelFrameFlyweight.data(bb), - Unpooled.wrappedBuffer("abc1".getBytes())); + RequestChannelFrameCodec.data(bb), Unpooled.wrappedBuffer("abc1".getBytes())); } return false; @@ -964,7 +958,7 @@ public void ensuresCorrectOrderOfStreamIdIssuingInCaseOfRacing( () -> publisher2.subscribe(AssertSubscriber.create())); Assertions.assertThat(rule.connection.getSent()) - .extracting(FrameHeaderFlyweight::streamId) + .extracting(FrameHeaderCodec::streamId) .containsExactly(i, i + 2); rule.connection.getSent().clear(); } @@ -999,7 +993,7 @@ public int sendRequestResponse(Publisher response) { response.subscribe(sub); int streamId = rule.getStreamIdForRequestType(REQUEST_RESPONSE); rule.connection.addToReceivedBuffer( - PayloadFrameFlyweight.encodeNextCompleteReleasingPayload( + PayloadFrameCodec.encodeNextCompleteReleasingPayload( rule.alloc(), streamId, EmptyPayload.INSTANCE)); verify(sub).onNext(any(Payload.class)); verify(sub).onComplete(); @@ -1028,7 +1022,7 @@ public int getStreamIdForRequestType(FrameType expectedFrameType) { for (ByteBuf frame : connection.getSent()) { FrameType frameType = frameType(frame); if (frameType == expectedFrameType) { - return FrameHeaderFlyweight.streamId(frame); + return FrameHeaderCodec.streamId(frame); } framesFound.add(frameType); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index 9ec2a2df1..e34973848 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -17,7 +17,7 @@ package io.rsocket.core; import static io.rsocket.core.PayloadValidationUtils.INVALID_PAYLOAD_ERROR_MESSAGE; -import static io.rsocket.frame.FrameHeaderFlyweight.frameType; +import static io.rsocket.frame.FrameHeaderCodec.frameType; import static io.rsocket.frame.FrameType.ERROR; import static io.rsocket.frame.FrameType.REQUEST_CHANNEL; import static io.rsocket.frame.FrameType.REQUEST_FNF; @@ -38,18 +38,18 @@ import io.netty.util.ReferenceCounted; import io.rsocket.Payload; import io.rsocket.RSocket; -import io.rsocket.frame.CancelFrameFlyweight; -import io.rsocket.frame.ErrorFrameFlyweight; -import io.rsocket.frame.FrameHeaderFlyweight; -import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.frame.CancelFrameCodec; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.FrameHeaderCodec; +import io.rsocket.frame.FrameLengthCodec; import io.rsocket.frame.FrameType; -import io.rsocket.frame.KeepAliveFrameFlyweight; -import io.rsocket.frame.PayloadFrameFlyweight; -import io.rsocket.frame.RequestChannelFrameFlyweight; -import io.rsocket.frame.RequestFireAndForgetFrameFlyweight; -import io.rsocket.frame.RequestNFrameFlyweight; -import io.rsocket.frame.RequestResponseFrameFlyweight; -import io.rsocket.frame.RequestStreamFrameFlyweight; +import io.rsocket.frame.KeepAliveFrameCodec; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestChannelFrameCodec; +import io.rsocket.frame.RequestFireAndForgetFrameCodec; +import io.rsocket.frame.RequestNFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; +import io.rsocket.frame.RequestStreamFrameCodec; import io.rsocket.frame.decoder.PayloadDecoder; import io.rsocket.internal.subscriber.AssertSubscriber; import io.rsocket.lease.ResponderLeaseHandler; @@ -117,13 +117,13 @@ public void tearDown() { @Disabled public void testHandleKeepAlive() throws Exception { rule.connection.addToReceivedBuffer( - KeepAliveFrameFlyweight.encode(rule.alloc(), true, 0, Unpooled.EMPTY_BUFFER)); + KeepAliveFrameCodec.encode(rule.alloc(), true, 0, Unpooled.EMPTY_BUFFER)); ByteBuf sent = rule.connection.awaitSend(); assertThat("Unexpected frame sent.", frameType(sent), is(FrameType.KEEPALIVE)); /*Keep alive ack must not have respond flag else, it will result in infinite ping-pong of keep alive frames.*/ assertThat( "Unexpected keep-alive frame respond flag.", - KeepAliveFrameFlyweight.respondFlag(sent), + KeepAliveFrameCodec.respondFlag(sent), is(false)); } @@ -176,7 +176,7 @@ public Mono requestResponse(Payload payload) { assertThat("Unexpected error.", rule.errors, is(empty())); assertThat("Unexpected frame sent.", rule.connection.getSent(), is(empty())); - rule.connection.addToReceivedBuffer(CancelFrameFlyweight.encode(allocator, streamId)); + rule.connection.addToReceivedBuffer(CancelFrameCodec.encode(allocator, streamId)); assertThat("Unexpected frame sent.", rule.connection.getSent(), is(empty())); assertThat("Subscription not cancelled.", cancelled.get(), is(true)); @@ -188,8 +188,8 @@ public Mono requestResponse(Payload payload) { public void shouldThrownExceptionIfGivenPayloadIsExitsSizeAllowanceWithNoFragmentation() { final int streamId = 4; final AtomicBoolean cancelled = new AtomicBoolean(); - byte[] metadata = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; - byte[] data = new byte[FrameLengthFlyweight.FRAME_LENGTH_MASK]; + byte[] metadata = new byte[FrameLengthCodec.FRAME_LENGTH_MASK]; + byte[] data = new byte[FrameLengthCodec.FRAME_LENGTH_MASK]; ThreadLocalRandom.current().nextBytes(metadata); ThreadLocalRandom.current().nextBytes(data); final Payload payload = DefaultPayload.create(data, metadata); @@ -239,8 +239,8 @@ protected void hookOnSubscribe(Subscription subscription) { Assertions.assertThat(rule.connection.getSent()) .hasSize(1) .first() - .matches(bb -> FrameHeaderFlyweight.frameType(bb) == FrameType.ERROR) - .matches(bb -> ErrorFrameFlyweight.dataUtf8(bb).contains(INVALID_PAYLOAD_ERROR_MESSAGE)) + .matches(bb -> FrameHeaderCodec.frameType(bb) == FrameType.ERROR) + .matches(bb -> ErrorFrameCodec.dataUtf8(bb).contains(INVALID_PAYLOAD_ERROR_MESSAGE)) .matches(ReferenceCounted::release); assertThat("Subscription not cancelled.", cancelled.get(), is(true)); @@ -272,21 +272,21 @@ public Flux requestChannel(Publisher payloads) { ByteBuf data1 = allocator.buffer(); data1.writeCharSequence("def1", CharsetUtil.UTF_8); ByteBuf nextFrame1 = - PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata1, data1); + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata1, data1); ByteBuf metadata2 = allocator.buffer(); metadata2.writeCharSequence("abc2", CharsetUtil.UTF_8); ByteBuf data2 = allocator.buffer(); data2.writeCharSequence("def2", CharsetUtil.UTF_8); ByteBuf nextFrame2 = - PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata2, data2); + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata2, data2); ByteBuf metadata3 = allocator.buffer(); metadata3.writeCharSequence("abc3", CharsetUtil.UTF_8); ByteBuf data3 = allocator.buffer(); data3.writeCharSequence("def3", CharsetUtil.UTF_8); ByteBuf nextFrame3 = - PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata3, data3); + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata3, data3); RaceTestUtils.race( () -> { @@ -325,7 +325,7 @@ public Flux requestChannel(Publisher payloads) { rule.sendRequest(1, REQUEST_CHANNEL); - ByteBuf cancelFrame = CancelFrameFlyweight.encode(allocator, 1); + ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, 1); FluxSink sink = sinks[0]; RaceTestUtils.race( () -> rule.connection.addToReceivedBuffer(cancelFrame), @@ -365,8 +365,8 @@ public Flux requestChannel(Publisher payloads) { rule.sendRequest(1, REQUEST_CHANNEL); - ByteBuf cancelFrame = CancelFrameFlyweight.encode(allocator, 1); - ByteBuf requestNFrame = RequestNFrameFlyweight.encode(allocator, 1, Integer.MAX_VALUE); + ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, 1); + ByteBuf requestNFrame = RequestNFrameCodec.encode(allocator, 1, Integer.MAX_VALUE); FluxSink sink = sinks[0]; RaceTestUtils.race( () -> @@ -418,23 +418,23 @@ public Flux requestChannel(Publisher payloads) { ByteBuf data1 = allocator.buffer(); data1.writeCharSequence("def1", CharsetUtil.UTF_8); ByteBuf nextFrame1 = - PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata1, data1); + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata1, data1); ByteBuf metadata2 = allocator.buffer(); metadata2.writeCharSequence("abc2", CharsetUtil.UTF_8); ByteBuf data2 = allocator.buffer(); data2.writeCharSequence("def2", CharsetUtil.UTF_8); ByteBuf nextFrame2 = - PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata2, data2); + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata2, data2); ByteBuf metadata3 = allocator.buffer(); metadata3.writeCharSequence("abc3", CharsetUtil.UTF_8); ByteBuf data3 = allocator.buffer(); data3.writeCharSequence("def3", CharsetUtil.UTF_8); ByteBuf nextFrame3 = - PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata3, data3); + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata3, data3); - ByteBuf requestNFrame = RequestNFrameFlyweight.encode(allocator, 1, Integer.MAX_VALUE); + ByteBuf requestNFrame = RequestNFrameCodec.encode(allocator, 1, Integer.MAX_VALUE); FluxSink sink = sinks[0]; RaceTestUtils.race( @@ -477,7 +477,7 @@ public Flux requestStream(Payload payload) { rule.sendRequest(1, REQUEST_STREAM); - ByteBuf cancelFrame = CancelFrameFlyweight.encode(allocator, 1); + ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, 1); FluxSink sink = sinks[0]; RaceTestUtils.race( () -> rule.connection.addToReceivedBuffer(cancelFrame), @@ -520,7 +520,7 @@ public void subscribe(CoreSubscriber actual) { rule.sendRequest(1, REQUEST_RESPONSE); - ByteBuf cancelFrame = CancelFrameFlyweight.encode(allocator, 1); + ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, 1); RaceTestUtils.race( () -> rule.connection.addToReceivedBuffer(cancelFrame), () -> { @@ -551,7 +551,7 @@ public Flux requestStream(Payload payload) { rule.sendRequest(1, REQUEST_STREAM); - ByteBuf cancelFrame = CancelFrameFlyweight.encode(allocator, 1); + ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, 1); FluxSink sink = sinks[0]; sink.next(ByteBufPayload.create("d1", "m1")); @@ -579,28 +579,28 @@ public Flux requestChannel(Publisher payloads) { rule.sendRequest(1, REQUEST_STREAM); - ByteBuf cancelFrame = CancelFrameFlyweight.encode(allocator, 1); + ByteBuf cancelFrame = CancelFrameCodec.encode(allocator, 1); ByteBuf metadata1 = allocator.buffer(); metadata1.writeCharSequence("abc1", CharsetUtil.UTF_8); ByteBuf data1 = allocator.buffer(); data1.writeCharSequence("def1", CharsetUtil.UTF_8); ByteBuf nextFrame1 = - PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata1, data1); + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata1, data1); ByteBuf metadata2 = allocator.buffer(); metadata2.writeCharSequence("abc2", CharsetUtil.UTF_8); ByteBuf data2 = allocator.buffer(); data2.writeCharSequence("def2", CharsetUtil.UTF_8); ByteBuf nextFrame2 = - PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata2, data2); + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata2, data2); ByteBuf metadata3 = allocator.buffer(); metadata3.writeCharSequence("abc3", CharsetUtil.UTF_8); ByteBuf data3 = allocator.buffer(); data3.writeCharSequence("de3", CharsetUtil.UTF_8); ByteBuf nextFrame3 = - PayloadFrameFlyweight.encode(allocator, 1, false, false, true, metadata3, data3); + PayloadFrameCodec.encode(allocator, 1, false, false, true, metadata3, data3); rule.connection.addToReceivedBuffer(nextFrame1, nextFrame2, nextFrame3); rule.connection.addToReceivedBuffer(cancelFrame); @@ -651,7 +651,7 @@ public Flux requestChannel(Publisher payloads) { // if responses number is bigger than 1 we have to send one extra requestN if (responsesCnt > 1) { rule.connection.addToReceivedBuffer( - RequestNFrameFlyweight.encode(allocator, 1, responsesCnt - 1)); + RequestNFrameCodec.encode(allocator, 1, responsesCnt - 1)); } // respond with specific number of elements @@ -663,7 +663,7 @@ public Flux requestChannel(Publisher payloads) { if (framesCnt > 1) { for (int i = 1; i < responsesCnt; i++) { rule.connection.addToReceivedBuffer( - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, false, @@ -680,7 +680,7 @@ public Flux requestChannel(Publisher payloads) { .describedAs( "Interaction Type :[%s]. Expected to observe %s frames sent", frameType, responsesCnt) .hasSize(responsesCnt) - .allMatch(bb -> !FrameHeaderFlyweight.hasMetadata(bb)); + .allMatch(bb -> !FrameHeaderCodec.hasMetadata(bb)); } if (framesCnt > 1) { @@ -691,7 +691,7 @@ public Flux requestChannel(Publisher payloads) { frameType, framesCnt - 1) .hasSize(1) .first() - .matches(bb -> RequestNFrameFlyweight.requestN(bb) == (framesCnt - 1)); + .matches(bb -> RequestNFrameCodec.requestN(bb) == (framesCnt - 1)); } Assertions.assertThat(rule.connection.getSent()).allMatch(ReferenceCounted::release); @@ -813,22 +813,20 @@ private void sendRequest(int streamId, FrameType frameType, Payload payload) { switch (frameType) { case REQUEST_CHANNEL: request = - RequestChannelFrameFlyweight.encodeReleasingPayload( + RequestChannelFrameCodec.encodeReleasingPayload( allocator, streamId, false, prefetch, payload); break; case REQUEST_STREAM: request = - RequestStreamFrameFlyweight.encodeReleasingPayload( + RequestStreamFrameCodec.encodeReleasingPayload( allocator, streamId, prefetch, payload); break; case REQUEST_RESPONSE: - request = - RequestResponseFrameFlyweight.encodeReleasingPayload(allocator, streamId, payload); + request = RequestResponseFrameCodec.encodeReleasingPayload(allocator, streamId, payload); break; case REQUEST_FNF: request = - RequestFireAndForgetFrameFlyweight.encodeReleasingPayload( - allocator, streamId, payload); + RequestFireAndForgetFrameCodec.encodeReleasingPayload(allocator, streamId, payload); break; default: throw new IllegalArgumentException("unsupported type: " + frameType); diff --git a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java index 388bfffeb..75a5e070e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -9,10 +9,10 @@ import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.exceptions.Exceptions; import io.rsocket.exceptions.RejectedSetupException; -import io.rsocket.frame.ErrorFrameFlyweight; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.ErrorFrameCodec; +import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; -import io.rsocket.frame.SetupFrameFlyweight; +import io.rsocket.frame.SetupFrameCodec; import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.transport.ServerTransport; @@ -39,7 +39,7 @@ void responderRejectSetup() { transport.connect(); ByteBuf sentFrame = transport.awaitSent(); - assertThat(FrameHeaderFlyweight.frameType(sentFrame)).isEqualTo(FrameType.ERROR); + assertThat(FrameHeaderCodec.frameType(sentFrame)).isEqualTo(FrameType.ERROR); RuntimeException error = Exceptions.from(0, sentFrame); assertThat(errorMsg).isEqualTo(error.getMessage()); assertThat(error).isInstanceOf(RejectedSetupException.class); @@ -75,7 +75,7 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { .doOnRequest( ignored -> conn.addToReceivedBuffer( - ErrorFrameFlyweight.encode( + ErrorFrameCodec.encode( ByteBufAllocator.DEFAULT, 0, new RejectedSetupException(errorMsg))))) @@ -106,8 +106,7 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { TestScheduler.INSTANCE); conn.addToReceivedBuffer( - ErrorFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, 0, new RejectedSetupException("error"))); + ErrorFrameCodec.encode(ByteBufAllocator.DEFAULT, 0, new RejectedSetupException("error"))); StepVerifier.create( rSocket @@ -158,8 +157,7 @@ public ByteBuf awaitSent() { public void connect() { Payload payload = DefaultPayload.create(DefaultPayload.EMPTY_BUFFER); - ByteBuf setup = - SetupFrameFlyweight.encode(allocator, false, 0, 42, "mdMime", "dMime", payload); + ByteBuf setup = SetupFrameCodec.encode(allocator, false, 0, 42, "mdMime", "dMime", payload); conn.addToReceivedBuffer(setup); } diff --git a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java index e646080c7..b3f596a37 100644 --- a/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java +++ b/rsocket-core/src/test/java/io/rsocket/exceptions/ExceptionsTest.java @@ -16,22 +16,22 @@ package io.rsocket.exceptions; -import static io.rsocket.frame.ErrorFrameFlyweight.APPLICATION_ERROR; -import static io.rsocket.frame.ErrorFrameFlyweight.CANCELED; -import static io.rsocket.frame.ErrorFrameFlyweight.CONNECTION_CLOSE; -import static io.rsocket.frame.ErrorFrameFlyweight.CONNECTION_ERROR; -import static io.rsocket.frame.ErrorFrameFlyweight.INVALID; -import static io.rsocket.frame.ErrorFrameFlyweight.INVALID_SETUP; -import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED; -import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED_RESUME; -import static io.rsocket.frame.ErrorFrameFlyweight.REJECTED_SETUP; -import static io.rsocket.frame.ErrorFrameFlyweight.UNSUPPORTED_SETUP; +import static io.rsocket.frame.ErrorFrameCodec.APPLICATION_ERROR; +import static io.rsocket.frame.ErrorFrameCodec.CANCELED; +import static io.rsocket.frame.ErrorFrameCodec.CONNECTION_CLOSE; +import static io.rsocket.frame.ErrorFrameCodec.CONNECTION_ERROR; +import static io.rsocket.frame.ErrorFrameCodec.INVALID; +import static io.rsocket.frame.ErrorFrameCodec.INVALID_SETUP; +import static io.rsocket.frame.ErrorFrameCodec.REJECTED; +import static io.rsocket.frame.ErrorFrameCodec.REJECTED_RESUME; +import static io.rsocket.frame.ErrorFrameCodec.REJECTED_SETUP; +import static io.rsocket.frame.ErrorFrameCodec.UNSUPPORTED_SETUP; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; import io.netty.buffer.ByteBuf; import io.netty.buffer.UnpooledByteBufAllocator; -import io.rsocket.frame.ErrorFrameFlyweight; +import io.rsocket.frame.ErrorFrameCodec; import java.util.concurrent.ThreadLocalRandom; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -205,9 +205,9 @@ void fromCustomRSocketException() { int randomCode = ThreadLocalRandom.current().nextBoolean() ? ThreadLocalRandom.current() - .nextInt(Integer.MIN_VALUE, ErrorFrameFlyweight.MAX_USER_ALLOWED_ERROR_CODE) + .nextInt(Integer.MIN_VALUE, ErrorFrameCodec.MAX_USER_ALLOWED_ERROR_CODE) : ThreadLocalRandom.current() - .nextInt(ErrorFrameFlyweight.MIN_USER_ALLOWED_ERROR_CODE, Integer.MAX_VALUE); + .nextInt(ErrorFrameCodec.MIN_USER_ALLOWED_ERROR_CODE, Integer.MAX_VALUE); ByteBuf byteBuf = createErrorFrame(0, randomCode, "test-message"); assertThat(Exceptions.from(1, byteBuf)) @@ -229,7 +229,7 @@ void fromWithNullFrame() { } private ByteBuf createErrorFrame(int streamId, int errorCode, String message) { - return ErrorFrameFlyweight.encode( + return ErrorFrameCodec.encode( UnpooledByteBufAllocator.DEFAULT, streamId, new TestRSocketException(errorCode, message)); } } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java index 9050eaa90..932df4283 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationDuplexConnectionTest.java @@ -87,7 +87,7 @@ void constructorNullDelegate() { @Test void sendData() { ByteBuf encode = - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( allocator, 1, false, Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer(data)); when(delegate.onClose()).thenReturn(Mono.never()); @@ -101,8 +101,8 @@ void sendData() { .expectNextCount(17) .assertNext( byteBuf -> { - Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); - Assert.assertFalse(FrameHeaderFlyweight.hasFollows(byteBuf)); + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertFalse(FrameHeaderCodec.hasFollows(byteBuf)); }) .verifyComplete(); } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java index a8569ef3b..ff62b56f2 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FragmentationIntegrationTest.java @@ -2,9 +2,9 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameUtil; -import io.rsocket.frame.PayloadFrameFlyweight; +import io.rsocket.frame.PayloadFrameCodec; import io.rsocket.util.DefaultPayload; import java.util.concurrent.ThreadLocalRandom; import org.junit.Assert; @@ -28,7 +28,7 @@ public class FragmentationIntegrationTest { @Test void fragmentAndReassembleData() { ByteBuf frame = - PayloadFrameFlyweight.encodeNextCompleteReleasingPayload( + PayloadFrameCodec.encodeNextCompleteReleasingPayload( allocator, 2, DefaultPayload.create(data)); System.out.println(FrameUtil.toString(frame)); @@ -36,7 +36,7 @@ void fragmentAndReassembleData() { Publisher fragments = FrameFragmenter.fragmentFrame( - allocator, 64, frame, FrameHeaderFlyweight.frameType(frame), false); + allocator, 64, frame, FrameHeaderCodec.frameType(frame), false); FrameReassembler reassembler = new FrameReassembler(allocator); @@ -50,9 +50,8 @@ void fragmentAndReassembleData() { String s = FrameUtil.toString(assembled); System.out.println(s); - Assert.assertEquals( - FrameHeaderFlyweight.frameType(frame), FrameHeaderFlyweight.frameType(assembled)); + Assert.assertEquals(FrameHeaderCodec.frameType(frame), FrameHeaderCodec.frameType(assembled)); Assert.assertEquals(frame.readableBytes(), assembled.readableBytes()); - Assert.assertEquals(PayloadFrameFlyweight.data(frame), PayloadFrameFlyweight.data(assembled)); + Assert.assertEquals(PayloadFrameCodec.data(frame), PayloadFrameCodec.data(assembled)); } } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java index c6b1735e6..60dbef74b 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameFragmenterTest.java @@ -42,16 +42,14 @@ final class FrameFragmenterTest { @Test void testGettingData() { ByteBuf rr = - RequestResponseFrameFlyweight.encode( - allocator, 1, true, null, Unpooled.wrappedBuffer(data)); + RequestResponseFrameCodec.encode(allocator, 1, true, null, Unpooled.wrappedBuffer(data)); ByteBuf fnf = - RequestFireAndForgetFrameFlyweight.encode( + RequestFireAndForgetFrameCodec.encode( allocator, 1, true, null, Unpooled.wrappedBuffer(data)); ByteBuf rs = - RequestStreamFrameFlyweight.encode( - allocator, 1, true, 1, null, Unpooled.wrappedBuffer(data)); + RequestStreamFrameCodec.encode(allocator, 1, true, 1, null, Unpooled.wrappedBuffer(data)); ByteBuf rc = - RequestChannelFrameFlyweight.encode( + RequestChannelFrameCodec.encode( allocator, 1, true, false, 1, null, Unpooled.wrappedBuffer(data)); ByteBuf data = FrameFragmenter.getData(rr, FrameType.REQUEST_RESPONSE); @@ -74,16 +72,16 @@ void testGettingData() { @Test void testGettingMetadata() { ByteBuf rr = - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); ByteBuf fnf = - RequestFireAndForgetFrameFlyweight.encode( + RequestFireAndForgetFrameCodec.encode( allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); ByteBuf rs = - RequestStreamFrameFlyweight.encode( + RequestStreamFrameCodec.encode( allocator, 1, true, 1, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); ByteBuf rc = - RequestChannelFrameFlyweight.encode( + RequestChannelFrameCodec.encode( allocator, 1, true, @@ -112,8 +110,7 @@ void testGettingMetadata() { @Test void returnEmptBufferWhenNoMetadataPresent() { ByteBuf rr = - RequestResponseFrameFlyweight.encode( - allocator, 1, true, null, Unpooled.wrappedBuffer(data)); + RequestResponseFrameCodec.encode(allocator, 1, true, null, Unpooled.wrappedBuffer(data)); ByteBuf data = FrameFragmenter.getMetadata(rr, FrameType.REQUEST_RESPONSE); Assert.assertEquals(data, Unpooled.EMPTY_BUFFER); @@ -124,8 +121,7 @@ void returnEmptBufferWhenNoMetadataPresent() { @Test void encodeFirstFrameWithData() { ByteBuf rr = - RequestResponseFrameFlyweight.encode( - allocator, 1, true, null, Unpooled.wrappedBuffer(data)); + RequestResponseFrameCodec.encode(allocator, 1, true, null, Unpooled.wrappedBuffer(data)); ByteBuf fragment = FrameFragmenter.encodeFirstFragment( @@ -138,22 +134,22 @@ void encodeFirstFrameWithData() { Unpooled.wrappedBuffer(data)); Assert.assertEquals(256, fragment.readableBytes()); - Assert.assertEquals(FrameType.REQUEST_RESPONSE, FrameHeaderFlyweight.frameType(fragment)); - Assert.assertEquals(1, FrameHeaderFlyweight.streamId(fragment)); - Assert.assertTrue(FrameHeaderFlyweight.hasFollows(fragment)); + Assert.assertEquals(FrameType.REQUEST_RESPONSE, FrameHeaderCodec.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(fragment)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(fragment)); - ByteBuf data = RequestResponseFrameFlyweight.data(fragment); + ByteBuf data = RequestResponseFrameCodec.data(fragment); ByteBuf byteBuf = Unpooled.wrappedBuffer(this.data).readSlice(data.readableBytes()); Assert.assertEquals(byteBuf, data); - Assert.assertFalse(FrameHeaderFlyweight.hasMetadata(fragment)); + Assert.assertFalse(FrameHeaderCodec.hasMetadata(fragment)); } @DisplayName("encode first channel frame") @Test void encodeFirstWithDataChannel() { ByteBuf rc = - RequestChannelFrameFlyweight.encode( + RequestChannelFrameCodec.encode( allocator, 1, true, false, 10, null, Unpooled.wrappedBuffer(data)); ByteBuf fragment = @@ -167,24 +163,23 @@ void encodeFirstWithDataChannel() { Unpooled.wrappedBuffer(data)); Assert.assertEquals(256, fragment.readableBytes()); - Assert.assertEquals(FrameType.REQUEST_CHANNEL, FrameHeaderFlyweight.frameType(fragment)); - Assert.assertEquals(1, FrameHeaderFlyweight.streamId(fragment)); - Assert.assertEquals(10, RequestChannelFrameFlyweight.initialRequestN(fragment)); - Assert.assertTrue(FrameHeaderFlyweight.hasFollows(fragment)); + Assert.assertEquals(FrameType.REQUEST_CHANNEL, FrameHeaderCodec.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(fragment)); + Assert.assertEquals(10, RequestChannelFrameCodec.initialRequestN(fragment)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(fragment)); - ByteBuf data = RequestChannelFrameFlyweight.data(fragment); + ByteBuf data = RequestChannelFrameCodec.data(fragment); ByteBuf byteBuf = Unpooled.wrappedBuffer(this.data).readSlice(data.readableBytes()); Assert.assertEquals(byteBuf, data); - Assert.assertFalse(FrameHeaderFlyweight.hasMetadata(fragment)); + Assert.assertFalse(FrameHeaderCodec.hasMetadata(fragment)); } @DisplayName("encode first stream frame") @Test void encodeFirstWithDataStream() { ByteBuf rc = - RequestStreamFrameFlyweight.encode( - allocator, 1, true, 50, null, Unpooled.wrappedBuffer(data)); + RequestStreamFrameCodec.encode(allocator, 1, true, 50, null, Unpooled.wrappedBuffer(data)); ByteBuf fragment = FrameFragmenter.encodeFirstFragment( @@ -197,23 +192,23 @@ void encodeFirstWithDataStream() { Unpooled.wrappedBuffer(data)); Assert.assertEquals(256, fragment.readableBytes()); - Assert.assertEquals(FrameType.REQUEST_STREAM, FrameHeaderFlyweight.frameType(fragment)); - Assert.assertEquals(1, FrameHeaderFlyweight.streamId(fragment)); - Assert.assertEquals(50, RequestStreamFrameFlyweight.initialRequestN(fragment)); - Assert.assertTrue(FrameHeaderFlyweight.hasFollows(fragment)); + Assert.assertEquals(FrameType.REQUEST_STREAM, FrameHeaderCodec.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(fragment)); + Assert.assertEquals(50, RequestStreamFrameCodec.initialRequestN(fragment)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(fragment)); - ByteBuf data = RequestStreamFrameFlyweight.data(fragment); + ByteBuf data = RequestStreamFrameCodec.data(fragment); ByteBuf byteBuf = Unpooled.wrappedBuffer(this.data).readSlice(data.readableBytes()); Assert.assertEquals(byteBuf, data); - Assert.assertFalse(FrameHeaderFlyweight.hasMetadata(fragment)); + Assert.assertFalse(FrameHeaderCodec.hasMetadata(fragment)); } @DisplayName("encode first frame with only metadata") @Test void encodeFirstFrameWithMetadata() { ByteBuf rr = - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER); ByteBuf fragment = @@ -227,21 +222,21 @@ void encodeFirstFrameWithMetadata() { Unpooled.EMPTY_BUFFER); Assert.assertEquals(256, fragment.readableBytes()); - Assert.assertEquals(FrameType.REQUEST_RESPONSE, FrameHeaderFlyweight.frameType(fragment)); - Assert.assertEquals(1, FrameHeaderFlyweight.streamId(fragment)); - Assert.assertTrue(FrameHeaderFlyweight.hasFollows(fragment)); + Assert.assertEquals(FrameType.REQUEST_RESPONSE, FrameHeaderCodec.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(fragment)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(fragment)); - ByteBuf data = RequestResponseFrameFlyweight.data(fragment); + ByteBuf data = RequestResponseFrameCodec.data(fragment); Assert.assertEquals(data, Unpooled.EMPTY_BUFFER); - Assert.assertTrue(FrameHeaderFlyweight.hasMetadata(fragment)); + Assert.assertTrue(FrameHeaderCodec.hasMetadata(fragment)); } @DisplayName("encode first stream frame with data and metadata") @Test void encodeFirstWithDataAndMetadataStream() { ByteBuf rc = - RequestStreamFrameFlyweight.encode( + RequestStreamFrameCodec.encode( allocator, 1, true, 50, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); ByteBuf fragment = @@ -255,27 +250,26 @@ void encodeFirstWithDataAndMetadataStream() { Unpooled.wrappedBuffer(data)); Assert.assertEquals(256, fragment.readableBytes()); - Assert.assertEquals(FrameType.REQUEST_STREAM, FrameHeaderFlyweight.frameType(fragment)); - Assert.assertEquals(1, FrameHeaderFlyweight.streamId(fragment)); - Assert.assertEquals(50, RequestStreamFrameFlyweight.initialRequestN(fragment)); - Assert.assertTrue(FrameHeaderFlyweight.hasFollows(fragment)); + Assert.assertEquals(FrameType.REQUEST_STREAM, FrameHeaderCodec.frameType(fragment)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(fragment)); + Assert.assertEquals(50, RequestStreamFrameCodec.initialRequestN(fragment)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(fragment)); - ByteBuf data = RequestStreamFrameFlyweight.data(fragment); + ByteBuf data = RequestStreamFrameCodec.data(fragment); Assert.assertEquals(0, data.readableBytes()); - ByteBuf metadata = RequestStreamFrameFlyweight.metadata(fragment); + ByteBuf metadata = RequestStreamFrameCodec.metadata(fragment); ByteBuf byteBuf = Unpooled.wrappedBuffer(this.metadata).readSlice(metadata.readableBytes()); Assert.assertEquals(byteBuf, metadata); - Assert.assertTrue(FrameHeaderFlyweight.hasMetadata(fragment)); + Assert.assertTrue(FrameHeaderCodec.hasMetadata(fragment)); } @DisplayName("fragments frame with only data") @Test void fragmentData() { ByteBuf rr = - RequestResponseFrameFlyweight.encode( - allocator, 1, true, null, Unpooled.wrappedBuffer(data)); + RequestResponseFrameCodec.encode(allocator, 1, true, null, Unpooled.wrappedBuffer(data)); Publisher fragments = FrameFragmenter.fragmentFrame(allocator, 1024, rr, FrameType.REQUEST_RESPONSE, false); @@ -284,15 +278,15 @@ void fragmentData() { .expectNextCount(1) .assertNext( byteBuf -> { - Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); - Assert.assertEquals(1, FrameHeaderFlyweight.streamId(byteBuf)); - Assert.assertTrue(FrameHeaderFlyweight.hasFollows(byteBuf)); + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(byteBuf)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(byteBuf)); }) .expectNextCount(2) .assertNext( byteBuf -> { - Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); - Assert.assertFalse(FrameHeaderFlyweight.hasFollows(byteBuf)); + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertFalse(FrameHeaderCodec.hasFollows(byteBuf)); }) .verifyComplete(); } @@ -301,7 +295,7 @@ void fragmentData() { @Test void fragmentMetadata() { ByteBuf rr = - RequestStreamFrameFlyweight.encode( + RequestStreamFrameCodec.encode( allocator, 1, true, 10, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER); Publisher fragments = @@ -311,15 +305,15 @@ void fragmentMetadata() { .expectNextCount(1) .assertNext( byteBuf -> { - Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); - Assert.assertEquals(1, FrameHeaderFlyweight.streamId(byteBuf)); - Assert.assertTrue(FrameHeaderFlyweight.hasFollows(byteBuf)); + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertEquals(1, FrameHeaderCodec.streamId(byteBuf)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(byteBuf)); }) .expectNextCount(2) .assertNext( byteBuf -> { - Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); - Assert.assertFalse(FrameHeaderFlyweight.hasFollows(byteBuf)); + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertFalse(FrameHeaderCodec.hasFollows(byteBuf)); }) .verifyComplete(); } @@ -328,7 +322,7 @@ void fragmentMetadata() { @Test void fragmentDataAndMetadata() { ByteBuf rr = - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)); Publisher fragments = @@ -337,20 +331,19 @@ void fragmentDataAndMetadata() { StepVerifier.create(Flux.from(fragments).doOnError(Throwable::printStackTrace)) .assertNext( byteBuf -> { - Assert.assertEquals( - FrameType.REQUEST_RESPONSE, FrameHeaderFlyweight.frameType(byteBuf)); - Assert.assertTrue(FrameHeaderFlyweight.hasFollows(byteBuf)); + Assert.assertEquals(FrameType.REQUEST_RESPONSE, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(byteBuf)); }) .expectNextCount(6) .assertNext( byteBuf -> { - Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); - Assert.assertTrue(FrameHeaderFlyweight.hasFollows(byteBuf)); + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertTrue(FrameHeaderCodec.hasFollows(byteBuf)); }) .assertNext( byteBuf -> { - Assert.assertEquals(FrameType.NEXT, FrameHeaderFlyweight.frameType(byteBuf)); - Assert.assertFalse(FrameHeaderFlyweight.hasFollows(byteBuf)); + Assert.assertEquals(FrameType.NEXT, FrameHeaderCodec.frameType(byteBuf)); + Assert.assertFalse(FrameHeaderCodec.hasFollows(byteBuf)); }) .verifyComplete(); } diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java index 13632165b..56f7fcf90 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/FrameReassemblerTest.java @@ -47,15 +47,15 @@ final class FrameReassemblerTest { void reassembleData() { List byteBufs = Arrays.asList( - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( allocator, 1, true, null, Unpooled.wrappedBuffer(data)), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, false, false, true, null, Unpooled.wrappedBuffer(data))); FrameReassembler reassembler = new FrameReassembler(allocator); @@ -76,7 +76,7 @@ void reassembleData() { StepVerifier.create(assembled) .assertNext( byteBuf -> { - Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); + Assert.assertEquals(data, RequestResponseFrameCodec.data(byteBuf)); ReferenceCountUtil.safeRelease(byteBuf); }) .verifyComplete(); @@ -88,7 +88,7 @@ void reassembleData() { void passthrough() { List byteBufs = Arrays.asList( - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( allocator, 1, false, null, Unpooled.wrappedBuffer(data))); FrameReassembler reassembler = new FrameReassembler(allocator); @@ -103,7 +103,7 @@ void passthrough() { StepVerifier.create(assembled) .assertNext( byteBuf -> { - Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); + Assert.assertEquals(data, RequestResponseFrameCodec.data(byteBuf)); ReferenceCountUtil.safeRelease(byteBuf); }) .verifyComplete(); @@ -115,9 +115,9 @@ void passthrough() { void reassembleMetadata() { List byteBufs = Arrays.asList( - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -125,7 +125,7 @@ void reassembleMetadata() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -133,7 +133,7 @@ void reassembleMetadata() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -141,7 +141,7 @@ void reassembleMetadata() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, false, @@ -169,7 +169,7 @@ void reassembleMetadata() { .assertNext( byteBuf -> { System.out.println(byteBuf.readableBytes()); - ByteBuf m = RequestResponseFrameFlyweight.metadata(byteBuf); + ByteBuf m = RequestResponseFrameCodec.metadata(byteBuf); Assert.assertEquals(metadata, m); }) .verifyComplete(); @@ -180,7 +180,7 @@ void reassembleMetadata() { void reassembleMetadataChannel() { List byteBufs = Arrays.asList( - RequestChannelFrameFlyweight.encode( + RequestChannelFrameCodec.encode( allocator, 1, true, @@ -188,7 +188,7 @@ void reassembleMetadataChannel() { 100, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -196,7 +196,7 @@ void reassembleMetadataChannel() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -204,7 +204,7 @@ void reassembleMetadataChannel() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -212,7 +212,7 @@ void reassembleMetadataChannel() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, false, @@ -240,9 +240,9 @@ void reassembleMetadataChannel() { .assertNext( byteBuf -> { System.out.println(byteBuf.readableBytes()); - ByteBuf m = RequestChannelFrameFlyweight.metadata(byteBuf); + ByteBuf m = RequestChannelFrameCodec.metadata(byteBuf); Assert.assertEquals(metadata, m); - Assert.assertEquals(100, RequestChannelFrameFlyweight.initialRequestN(byteBuf)); + Assert.assertEquals(100, RequestChannelFrameCodec.initialRequestN(byteBuf)); ReferenceCountUtil.safeRelease(byteBuf); }) .verifyComplete(); @@ -255,9 +255,9 @@ void reassembleMetadataChannel() { void reassembleMetadataStream() { List byteBufs = Arrays.asList( - RequestStreamFrameFlyweight.encode( + RequestStreamFrameCodec.encode( allocator, 1, true, 250, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -265,7 +265,7 @@ void reassembleMetadataStream() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -273,7 +273,7 @@ void reassembleMetadataStream() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -281,7 +281,7 @@ void reassembleMetadataStream() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, false, @@ -309,9 +309,9 @@ void reassembleMetadataStream() { .assertNext( byteBuf -> { System.out.println(byteBuf.readableBytes()); - ByteBuf m = RequestStreamFrameFlyweight.metadata(byteBuf); + ByteBuf m = RequestStreamFrameCodec.metadata(byteBuf); Assert.assertEquals(metadata, m); - Assert.assertEquals(250, RequestChannelFrameFlyweight.initialRequestN(byteBuf)); + Assert.assertEquals(250, RequestChannelFrameCodec.initialRequestN(byteBuf)); ReferenceCountUtil.safeRelease(byteBuf); }) .verifyComplete(); @@ -325,9 +325,9 @@ void reassembleMetadataAndData() { List byteBufs = Arrays.asList( - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -335,7 +335,7 @@ void reassembleMetadataAndData() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -343,7 +343,7 @@ void reassembleMetadataAndData() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -351,7 +351,7 @@ void reassembleMetadataAndData() { true, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, false, false, true, null, Unpooled.wrappedBuffer(data))); FrameReassembler reassembler = new FrameReassembler(allocator); @@ -379,8 +379,8 @@ void reassembleMetadataAndData() { StepVerifier.create(assembled) .assertNext( byteBuf -> { - Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); - Assert.assertEquals(metadata, RequestResponseFrameFlyweight.metadata(byteBuf)); + Assert.assertEquals(data, RequestResponseFrameCodec.data(byteBuf)); + Assert.assertEquals(metadata, RequestResponseFrameCodec.metadata(byteBuf)); }) .verifyComplete(); ReferenceCountUtil.safeRelease(data); @@ -392,9 +392,9 @@ void reassembleMetadataAndData() { public void cancelBeforeAssembling() { List byteBufs = Arrays.asList( - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -402,7 +402,7 @@ public void cancelBeforeAssembling() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -410,7 +410,7 @@ public void cancelBeforeAssembling() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -426,7 +426,7 @@ public void cancelBeforeAssembling() { Assert.assertTrue(reassembler.metadata.containsKey(1)); Assert.assertTrue(reassembler.data.containsKey(1)); - Flux.just(CancelFrameFlyweight.encode(allocator, 1)) + Flux.just(CancelFrameCodec.encode(allocator, 1)) .handle(reassembler::reassembleFrame) .blockLast(); @@ -440,9 +440,9 @@ public void cancelBeforeAssembling() { public void dispose() { List byteBufs = Arrays.asList( - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -450,7 +450,7 @@ public void dispose() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -458,7 +458,7 @@ public void dispose() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, diff --git a/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java b/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java index 013e2ebc2..b083d6841 100644 --- a/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/fragmentation/ReassembleDuplexConnectionTest.java @@ -26,11 +26,11 @@ import io.netty.buffer.Unpooled; import io.rsocket.DuplexConnection; import io.rsocket.buffer.LeaksTrackingByteBufAllocator; -import io.rsocket.frame.CancelFrameFlyweight; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.CancelFrameCodec; +import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; -import io.rsocket.frame.PayloadFrameFlyweight; -import io.rsocket.frame.RequestResponseFrameFlyweight; +import io.rsocket.frame.PayloadFrameCodec; +import io.rsocket.frame.RequestResponseFrameCodec; import java.util.Arrays; import java.util.List; import java.util.concurrent.ThreadLocalRandom; @@ -60,15 +60,15 @@ final class ReassembleDuplexConnectionTest { void reassembleData() { List byteBufs = Arrays.asList( - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( allocator, 1, true, null, Unpooled.wrappedBuffer(data)), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, false, true, null, Unpooled.wrappedBuffer(data)), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, false, false, true, null, Unpooled.wrappedBuffer(data))); CompositeByteBuf data = @@ -91,7 +91,7 @@ void reassembleData() { .as(StepVerifier::create) .assertNext( byteBuf -> { - Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); + Assert.assertEquals(data, RequestResponseFrameCodec.data(byteBuf)); }) .verifyComplete(); } @@ -101,9 +101,9 @@ void reassembleData() { void reassembleMetadata() { List byteBufs = Arrays.asList( - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -111,7 +111,7 @@ void reassembleMetadata() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -119,7 +119,7 @@ void reassembleMetadata() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -127,7 +127,7 @@ void reassembleMetadata() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, false, @@ -157,7 +157,7 @@ void reassembleMetadata() { .assertNext( byteBuf -> { System.out.println(byteBuf.readableBytes()); - ByteBuf m = RequestResponseFrameFlyweight.metadata(byteBuf); + ByteBuf m = RequestResponseFrameCodec.metadata(byteBuf); Assert.assertEquals(metadata, m); }) .verifyComplete(); @@ -168,9 +168,9 @@ void reassembleMetadata() { void reassembleMetadataAndData() { List byteBufs = Arrays.asList( - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( allocator, 1, true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -178,7 +178,7 @@ void reassembleMetadataAndData() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -186,7 +186,7 @@ void reassembleMetadataAndData() { true, Unpooled.wrappedBuffer(metadata), Unpooled.EMPTY_BUFFER), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, true, @@ -194,7 +194,7 @@ void reassembleMetadataAndData() { true, Unpooled.wrappedBuffer(metadata), Unpooled.wrappedBuffer(data)), - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 1, false, false, true, null, Unpooled.wrappedBuffer(data))); CompositeByteBuf data = @@ -224,8 +224,8 @@ void reassembleMetadataAndData() { .as(StepVerifier::create) .assertNext( byteBuf -> { - Assert.assertEquals(data, RequestResponseFrameFlyweight.data(byteBuf)); - Assert.assertEquals(metadata, RequestResponseFrameFlyweight.metadata(byteBuf)); + Assert.assertEquals(data, RequestResponseFrameCodec.data(byteBuf)); + Assert.assertEquals(metadata, RequestResponseFrameCodec.metadata(byteBuf)); }) .verifyComplete(); } @@ -234,8 +234,7 @@ void reassembleMetadataAndData() { @Test void reassembleNonFragment() { ByteBuf encode = - RequestResponseFrameFlyweight.encode( - allocator, 1, false, null, Unpooled.wrappedBuffer(data)); + RequestResponseFrameCodec.encode(allocator, 1, false, null, Unpooled.wrappedBuffer(data)); when(delegate.receive()).thenReturn(Flux.just(encode)); when(delegate.onClose()).thenReturn(Mono.never()); @@ -247,7 +246,7 @@ void reassembleNonFragment() { .assertNext( byteBuf -> { Assert.assertEquals( - Unpooled.wrappedBuffer(data), RequestResponseFrameFlyweight.data(byteBuf)); + Unpooled.wrappedBuffer(data), RequestResponseFrameCodec.data(byteBuf)); }) .verifyComplete(); } @@ -255,7 +254,7 @@ void reassembleNonFragment() { @DisplayName("does not reassemble non fragmentable frame") @Test void reassembleNonFragmentableFrame() { - ByteBuf encode = CancelFrameFlyweight.encode(allocator, 2); + ByteBuf encode = CancelFrameCodec.encode(allocator, 2); when(delegate.receive()).thenReturn(Flux.just(encode)); when(delegate.onClose()).thenReturn(Mono.never()); @@ -266,7 +265,7 @@ void reassembleNonFragmentableFrame() { .as(StepVerifier::create) .assertNext( byteBuf -> { - Assert.assertEquals(FrameType.CANCEL, FrameHeaderFlyweight.frameType(byteBuf)); + Assert.assertEquals(FrameType.CANCEL, FrameHeaderCodec.frameType(byteBuf)); }) .verifyComplete(); } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ErrorFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ErrorFrameCodecTest.java similarity index 65% rename from rsocket-core/src/test/java/io/rsocket/frame/ErrorFrameFlyweightTest.java rename to rsocket-core/src/test/java/io/rsocket/frame/ErrorFrameCodecTest.java index fa663432c..dc04c1141 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/ErrorFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/ErrorFrameCodecTest.java @@ -8,13 +8,13 @@ import io.rsocket.exceptions.ApplicationErrorException; import org.junit.jupiter.api.Test; -class ErrorFrameFlyweightTest { +class ErrorFrameCodecTest { @Test void testEncode() { ByteBuf frame = - ErrorFrameFlyweight.encode(ByteBufAllocator.DEFAULT, 1, new ApplicationErrorException("d")); + ErrorFrameCodec.encode(ByteBufAllocator.DEFAULT, 1, new ApplicationErrorException("d")); - frame = FrameLengthFlyweight.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); + frame = FrameLengthCodec.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); assertEquals("00000b000000012c000000020164", ByteBufUtil.hexDump(frame)); frame.release(); } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameCodecTest.java new file mode 100644 index 000000000..28209393e --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameCodecTest.java @@ -0,0 +1,62 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class ExtensionFrameCodecTest { + + @Test + void extensionDataMetadata() { + ByteBuf metadata = bytebuf("md"); + ByteBuf data = bytebuf("d"); + int extendedType = 1; + + ByteBuf extension = + ExtensionFrameCodec.encode(ByteBufAllocator.DEFAULT, 1, extendedType, metadata, data); + + Assertions.assertTrue(FrameHeaderCodec.hasMetadata(extension)); + Assertions.assertEquals(extendedType, ExtensionFrameCodec.extendedType(extension)); + Assertions.assertEquals(metadata, ExtensionFrameCodec.metadata(extension)); + Assertions.assertEquals(data, ExtensionFrameCodec.data(extension)); + extension.release(); + } + + @Test + void extensionData() { + ByteBuf data = bytebuf("d"); + int extendedType = 1; + + ByteBuf extension = + ExtensionFrameCodec.encode(ByteBufAllocator.DEFAULT, 1, extendedType, null, data); + + Assertions.assertFalse(FrameHeaderCodec.hasMetadata(extension)); + Assertions.assertEquals(extendedType, ExtensionFrameCodec.extendedType(extension)); + Assertions.assertNull(ExtensionFrameCodec.metadata(extension)); + Assertions.assertEquals(data, ExtensionFrameCodec.data(extension)); + extension.release(); + } + + @Test + void extensionMetadata() { + ByteBuf metadata = bytebuf("md"); + int extendedType = 1; + + ByteBuf extension = + ExtensionFrameCodec.encode( + ByteBufAllocator.DEFAULT, 1, extendedType, metadata, Unpooled.EMPTY_BUFFER); + + Assertions.assertTrue(FrameHeaderCodec.hasMetadata(extension)); + Assertions.assertEquals(extendedType, ExtensionFrameCodec.extendedType(extension)); + Assertions.assertEquals(metadata, ExtensionFrameCodec.metadata(extension)); + Assertions.assertEquals(0, ExtensionFrameCodec.data(extension).readableBytes()); + extension.release(); + } + + private static ByteBuf bytebuf(String str) { + return Unpooled.copiedBuffer(str, StandardCharsets.UTF_8); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameFlyweightTest.java deleted file mode 100644 index eea72c03e..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/ExtensionFrameFlyweightTest.java +++ /dev/null @@ -1,62 +0,0 @@ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import java.nio.charset.StandardCharsets; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class ExtensionFrameFlyweightTest { - - @Test - void extensionDataMetadata() { - ByteBuf metadata = bytebuf("md"); - ByteBuf data = bytebuf("d"); - int extendedType = 1; - - ByteBuf extension = - ExtensionFrameFlyweight.encode(ByteBufAllocator.DEFAULT, 1, extendedType, metadata, data); - - Assertions.assertTrue(FrameHeaderFlyweight.hasMetadata(extension)); - Assertions.assertEquals(extendedType, ExtensionFrameFlyweight.extendedType(extension)); - Assertions.assertEquals(metadata, ExtensionFrameFlyweight.metadata(extension)); - Assertions.assertEquals(data, ExtensionFrameFlyweight.data(extension)); - extension.release(); - } - - @Test - void extensionData() { - ByteBuf data = bytebuf("d"); - int extendedType = 1; - - ByteBuf extension = - ExtensionFrameFlyweight.encode(ByteBufAllocator.DEFAULT, 1, extendedType, null, data); - - Assertions.assertFalse(FrameHeaderFlyweight.hasMetadata(extension)); - Assertions.assertEquals(extendedType, ExtensionFrameFlyweight.extendedType(extension)); - Assertions.assertNull(ExtensionFrameFlyweight.metadata(extension)); - Assertions.assertEquals(data, ExtensionFrameFlyweight.data(extension)); - extension.release(); - } - - @Test - void extensionMetadata() { - ByteBuf metadata = bytebuf("md"); - int extendedType = 1; - - ByteBuf extension = - ExtensionFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, 1, extendedType, metadata, Unpooled.EMPTY_BUFFER); - - Assertions.assertTrue(FrameHeaderFlyweight.hasMetadata(extension)); - Assertions.assertEquals(extendedType, ExtensionFrameFlyweight.extendedType(extension)); - Assertions.assertEquals(metadata, ExtensionFrameFlyweight.metadata(extension)); - Assertions.assertEquals(0, ExtensionFrameFlyweight.data(extension).readableBytes()); - extension.release(); - } - - private static ByteBuf bytebuf(String str) { - return Unpooled.copiedBuffer(str, StandardCharsets.UTF_8); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderCodecTest.java similarity index 52% rename from rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderFlyweightTest.java rename to rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderCodecTest.java index a17fcc205..15788e631 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/FrameHeaderCodecTest.java @@ -7,7 +7,7 @@ import io.netty.buffer.ByteBufAllocator; import org.junit.jupiter.api.Test; -class FrameHeaderFlyweightTest { +class FrameHeaderCodecTest { // Taken from spec private static final int FRAME_MAX_SIZE = 16_777_215; @@ -15,10 +15,10 @@ class FrameHeaderFlyweightTest { void typeAndFlag() { FrameType frameType = FrameType.REQUEST_FNF; int flags = 0b1110110111; - ByteBuf header = FrameHeaderFlyweight.encode(ByteBufAllocator.DEFAULT, 0, frameType, flags); + ByteBuf header = FrameHeaderCodec.encode(ByteBufAllocator.DEFAULT, 0, frameType, flags); - assertEquals(flags, FrameHeaderFlyweight.flags(header)); - assertEquals(frameType, FrameHeaderFlyweight.frameType(header)); + assertEquals(flags, FrameHeaderCodec.flags(header)); + assertEquals(frameType, FrameHeaderCodec.frameType(header)); header.release(); } @@ -26,11 +26,11 @@ void typeAndFlag() { void typeAndFlagTruncated() { FrameType frameType = FrameType.SETUP; int flags = 0b11110110111; // 1 bit too many - ByteBuf header = FrameHeaderFlyweight.encode(ByteBufAllocator.DEFAULT, 0, frameType, flags); + ByteBuf header = FrameHeaderCodec.encode(ByteBufAllocator.DEFAULT, 0, frameType, flags); - assertNotEquals(flags, FrameHeaderFlyweight.flags(header)); - assertEquals(flags & 0b0000_0011_1111_1111, FrameHeaderFlyweight.flags(header)); - assertEquals(frameType, FrameHeaderFlyweight.frameType(header)); + assertNotEquals(flags, FrameHeaderCodec.flags(header)); + assertEquals(flags & 0b0000_0011_1111_1111, FrameHeaderCodec.flags(header)); + assertEquals(frameType, FrameHeaderCodec.frameType(header)); header.release(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/RequestFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/GenericFrameCodecTest.java similarity index 65% rename from rsocket-core/src/test/java/io/rsocket/frame/RequestFlyweightTest.java rename to rsocket-core/src/test/java/io/rsocket/frame/GenericFrameCodecTest.java index c19d4e1f4..ac19dc754 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/RequestFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/GenericFrameCodecTest.java @@ -9,11 +9,11 @@ import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; -class RequestFlyweightTest { +class GenericFrameCodecTest { @Test void testEncoding() { ByteBuf frame = - RequestStreamFrameFlyweight.encode( + RequestStreamFrameCodec.encode( ByteBufAllocator.DEFAULT, 1, false, @@ -21,7 +21,7 @@ void testEncoding() { Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - frame = FrameLengthFlyweight.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); + frame = FrameLengthCodec.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); // Encoded FrameLength⌍ ⌌ Encoded Headers // | | ⌌ Encoded Request(1) // | | | ⌌Encoded Metadata Length @@ -37,7 +37,7 @@ void testEncoding() { @Test void testEncodingWithEmptyMetadata() { ByteBuf frame = - RequestStreamFrameFlyweight.encode( + RequestStreamFrameCodec.encode( ByteBufAllocator.DEFAULT, 1, false, @@ -45,7 +45,7 @@ void testEncodingWithEmptyMetadata() { Unpooled.EMPTY_BUFFER, Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - frame = FrameLengthFlyweight.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); + frame = FrameLengthCodec.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); // Encoded FrameLength⌍ ⌌ Encoded Headers // | | ⌌ Encoded Request(1) // | | | ⌌Encoded Metadata Length (0) @@ -60,7 +60,7 @@ void testEncodingWithEmptyMetadata() { @Test void testEncodingWithNullMetadata() { ByteBuf frame = - RequestStreamFrameFlyweight.encode( + RequestStreamFrameCodec.encode( ByteBufAllocator.DEFAULT, 1, false, @@ -68,7 +68,7 @@ void testEncodingWithNullMetadata() { null, Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - frame = FrameLengthFlyweight.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); + frame = FrameLengthCodec.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); // Encoded FrameLength⌍ ⌌ Encoded Headers // | | ⌌ Encoded Request(1) @@ -83,18 +83,17 @@ void testEncodingWithNullMetadata() { @Test void requestResponseDataMetadata() { ByteBuf request = - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( ByteBufAllocator.DEFAULT, 1, false, Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - String data = RequestResponseFrameFlyweight.data(request).toString(StandardCharsets.UTF_8); - String metadata = - RequestResponseFrameFlyweight.metadata(request).toString(StandardCharsets.UTF_8); + String data = RequestResponseFrameCodec.data(request).toString(StandardCharsets.UTF_8); + String metadata = RequestResponseFrameCodec.metadata(request).toString(StandardCharsets.UTF_8); - assertTrue(FrameHeaderFlyweight.hasMetadata(request)); + assertTrue(FrameHeaderCodec.hasMetadata(request)); assertEquals("d", data); assertEquals("md", metadata); request.release(); @@ -103,17 +102,17 @@ void requestResponseDataMetadata() { @Test void requestResponseData() { ByteBuf request = - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( ByteBufAllocator.DEFAULT, 1, false, null, Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - String data = RequestResponseFrameFlyweight.data(request).toString(StandardCharsets.UTF_8); - ByteBuf metadata = RequestResponseFrameFlyweight.metadata(request); + String data = RequestResponseFrameCodec.data(request).toString(StandardCharsets.UTF_8); + ByteBuf metadata = RequestResponseFrameCodec.metadata(request); - assertFalse(FrameHeaderFlyweight.hasMetadata(request)); + assertFalse(FrameHeaderCodec.hasMetadata(request)); assertEquals("d", data); assertNull(metadata); request.release(); @@ -122,18 +121,17 @@ void requestResponseData() { @Test void requestResponseMetadata() { ByteBuf request = - RequestResponseFrameFlyweight.encode( + RequestResponseFrameCodec.encode( ByteBufAllocator.DEFAULT, 1, false, Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), Unpooled.EMPTY_BUFFER); - ByteBuf data = RequestResponseFrameFlyweight.data(request); - String metadata = - RequestResponseFrameFlyweight.metadata(request).toString(StandardCharsets.UTF_8); + ByteBuf data = RequestResponseFrameCodec.data(request); + String metadata = RequestResponseFrameCodec.metadata(request).toString(StandardCharsets.UTF_8); - assertTrue(FrameHeaderFlyweight.hasMetadata(request)); + assertTrue(FrameHeaderCodec.hasMetadata(request)); assertTrue(data.readableBytes() == 0); assertEquals("md", metadata); request.release(); @@ -142,7 +140,7 @@ void requestResponseMetadata() { @Test void requestStreamDataMetadata() { ByteBuf request = - RequestStreamFrameFlyweight.encode( + RequestStreamFrameCodec.encode( ByteBufAllocator.DEFAULT, 1, false, @@ -150,12 +148,11 @@ void requestStreamDataMetadata() { Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - long actualRequest = RequestStreamFrameFlyweight.initialRequestN(request); - String data = RequestStreamFrameFlyweight.data(request).toString(StandardCharsets.UTF_8); - String metadata = - RequestStreamFrameFlyweight.metadata(request).toString(StandardCharsets.UTF_8); + long actualRequest = RequestStreamFrameCodec.initialRequestN(request); + String data = RequestStreamFrameCodec.data(request).toString(StandardCharsets.UTF_8); + String metadata = RequestStreamFrameCodec.metadata(request).toString(StandardCharsets.UTF_8); - assertTrue(FrameHeaderFlyweight.hasMetadata(request)); + assertTrue(FrameHeaderCodec.hasMetadata(request)); assertEquals(Long.MAX_VALUE, actualRequest); assertEquals("md", metadata); assertEquals("d", data); @@ -165,7 +162,7 @@ void requestStreamDataMetadata() { @Test void requestStreamData() { ByteBuf request = - RequestStreamFrameFlyweight.encode( + RequestStreamFrameCodec.encode( ByteBufAllocator.DEFAULT, 1, false, @@ -173,11 +170,11 @@ void requestStreamData() { null, Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - long actualRequest = RequestStreamFrameFlyweight.initialRequestN(request); - String data = RequestStreamFrameFlyweight.data(request).toString(StandardCharsets.UTF_8); - ByteBuf metadata = RequestStreamFrameFlyweight.metadata(request); + long actualRequest = RequestStreamFrameCodec.initialRequestN(request); + String data = RequestStreamFrameCodec.data(request).toString(StandardCharsets.UTF_8); + ByteBuf metadata = RequestStreamFrameCodec.metadata(request); - assertFalse(FrameHeaderFlyweight.hasMetadata(request)); + assertFalse(FrameHeaderCodec.hasMetadata(request)); assertEquals(42L, actualRequest); assertNull(metadata); assertEquals("d", data); @@ -187,7 +184,7 @@ void requestStreamData() { @Test void requestStreamMetadata() { ByteBuf request = - RequestStreamFrameFlyweight.encode( + RequestStreamFrameCodec.encode( ByteBufAllocator.DEFAULT, 1, false, @@ -195,12 +192,11 @@ void requestStreamMetadata() { Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), Unpooled.EMPTY_BUFFER); - long actualRequest = RequestStreamFrameFlyweight.initialRequestN(request); - ByteBuf data = RequestStreamFrameFlyweight.data(request); - String metadata = - RequestStreamFrameFlyweight.metadata(request).toString(StandardCharsets.UTF_8); + long actualRequest = RequestStreamFrameCodec.initialRequestN(request); + ByteBuf data = RequestStreamFrameCodec.data(request); + String metadata = RequestStreamFrameCodec.metadata(request).toString(StandardCharsets.UTF_8); - assertTrue(FrameHeaderFlyweight.hasMetadata(request)); + assertTrue(FrameHeaderCodec.hasMetadata(request)); assertEquals(42L, actualRequest); assertTrue(data.readableBytes() == 0); assertEquals("md", metadata); @@ -210,18 +206,18 @@ void requestStreamMetadata() { @Test void requestFnfDataAndMetadata() { ByteBuf request = - RequestFireAndForgetFrameFlyweight.encode( + RequestFireAndForgetFrameCodec.encode( ByteBufAllocator.DEFAULT, 1, false, Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - String data = RequestFireAndForgetFrameFlyweight.data(request).toString(StandardCharsets.UTF_8); + String data = RequestFireAndForgetFrameCodec.data(request).toString(StandardCharsets.UTF_8); String metadata = - RequestFireAndForgetFrameFlyweight.metadata(request).toString(StandardCharsets.UTF_8); + RequestFireAndForgetFrameCodec.metadata(request).toString(StandardCharsets.UTF_8); - assertTrue(FrameHeaderFlyweight.hasMetadata(request)); + assertTrue(FrameHeaderCodec.hasMetadata(request)); assertEquals("d", data); assertEquals("md", metadata); request.release(); @@ -230,17 +226,17 @@ void requestFnfDataAndMetadata() { @Test void requestFnfData() { ByteBuf request = - RequestFireAndForgetFrameFlyweight.encode( + RequestFireAndForgetFrameCodec.encode( ByteBufAllocator.DEFAULT, 1, false, null, Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - String data = RequestFireAndForgetFrameFlyweight.data(request).toString(StandardCharsets.UTF_8); - ByteBuf metadata = RequestFireAndForgetFrameFlyweight.metadata(request); + String data = RequestFireAndForgetFrameCodec.data(request).toString(StandardCharsets.UTF_8); + ByteBuf metadata = RequestFireAndForgetFrameCodec.metadata(request); - assertFalse(FrameHeaderFlyweight.hasMetadata(request)); + assertFalse(FrameHeaderCodec.hasMetadata(request)); assertEquals("d", data); assertNull(metadata); request.release(); @@ -249,18 +245,18 @@ void requestFnfData() { @Test void requestFnfMetadata() { ByteBuf request = - RequestFireAndForgetFrameFlyweight.encode( + RequestFireAndForgetFrameCodec.encode( ByteBufAllocator.DEFAULT, 1, false, Unpooled.copiedBuffer("md", StandardCharsets.UTF_8), Unpooled.EMPTY_BUFFER); - ByteBuf data = RequestFireAndForgetFrameFlyweight.data(request); + ByteBuf data = RequestFireAndForgetFrameCodec.data(request); String metadata = - RequestFireAndForgetFrameFlyweight.metadata(request).toString(StandardCharsets.UTF_8); + RequestFireAndForgetFrameCodec.metadata(request).toString(StandardCharsets.UTF_8); - assertTrue(FrameHeaderFlyweight.hasMetadata(request)); + assertTrue(FrameHeaderCodec.hasMetadata(request)); assertEquals("md", metadata); assertTrue(data.readableBytes() == 0); request.release(); diff --git a/rsocket-core/src/test/java/io/rsocket/frame/KeepaliveFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/KeepaliveFrameFlyweightTest.java index eb55e89cd..bc013e024 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/KeepaliveFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/KeepaliveFrameFlyweightTest.java @@ -14,18 +14,18 @@ class KeepaliveFrameFlyweightTest { @Test void canReadData() { ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); - ByteBuf frame = KeepAliveFrameFlyweight.encode(ByteBufAllocator.DEFAULT, true, 0, data); - assertTrue(KeepAliveFrameFlyweight.respondFlag(frame)); - assertEquals(data, KeepAliveFrameFlyweight.data(frame)); + ByteBuf frame = KeepAliveFrameCodec.encode(ByteBufAllocator.DEFAULT, true, 0, data); + assertTrue(KeepAliveFrameCodec.respondFlag(frame)); + assertEquals(data, KeepAliveFrameCodec.data(frame)); frame.release(); } @Test void testEncoding() { ByteBuf frame = - KeepAliveFrameFlyweight.encode( + KeepAliveFrameCodec.encode( ByteBufAllocator.DEFAULT, true, 0, Unpooled.copiedBuffer("d", StandardCharsets.UTF_8)); - frame = FrameLengthFlyweight.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); + frame = FrameLengthCodec.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); assertEquals("00000f000000000c80000000000000000064", ByteBufUtil.hexDump(frame)); frame.release(); } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameCodecTest.java new file mode 100644 index 000000000..448b5003b --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameCodecTest.java @@ -0,0 +1,42 @@ +package io.rsocket.frame; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class LeaseFrameCodecTest { + + @Test + void leaseMetadata() { + ByteBuf metadata = bytebuf("md"); + int ttl = 1; + int numRequests = 42; + ByteBuf lease = LeaseFrameCodec.encode(ByteBufAllocator.DEFAULT, ttl, numRequests, metadata); + + Assertions.assertTrue(FrameHeaderCodec.hasMetadata(lease)); + Assertions.assertEquals(ttl, LeaseFrameCodec.ttl(lease)); + Assertions.assertEquals(numRequests, LeaseFrameCodec.numRequests(lease)); + Assertions.assertEquals(metadata, LeaseFrameCodec.metadata(lease)); + lease.release(); + } + + @Test + void leaseAbsentMetadata() { + int ttl = 1; + int numRequests = 42; + ByteBuf lease = LeaseFrameCodec.encode(ByteBufAllocator.DEFAULT, ttl, numRequests, null); + + Assertions.assertFalse(FrameHeaderCodec.hasMetadata(lease)); + Assertions.assertEquals(ttl, LeaseFrameCodec.ttl(lease)); + Assertions.assertEquals(numRequests, LeaseFrameCodec.numRequests(lease)); + Assertions.assertEquals(0, LeaseFrameCodec.metadata(lease).readableBytes()); + lease.release(); + } + + private static ByteBuf bytebuf(String str) { + return Unpooled.copiedBuffer(str, StandardCharsets.UTF_8); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameFlyweightTest.java deleted file mode 100644 index 0fc0c112b..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameFlyweightTest.java +++ /dev/null @@ -1,43 +0,0 @@ -package io.rsocket.frame; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import java.nio.charset.StandardCharsets; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; - -public class LeaseFrameFlyweightTest { - - @Test - void leaseMetadata() { - ByteBuf metadata = bytebuf("md"); - int ttl = 1; - int numRequests = 42; - ByteBuf lease = - LeaseFrameFlyweight.encode(ByteBufAllocator.DEFAULT, ttl, numRequests, metadata); - - Assertions.assertTrue(FrameHeaderFlyweight.hasMetadata(lease)); - Assertions.assertEquals(ttl, LeaseFrameFlyweight.ttl(lease)); - Assertions.assertEquals(numRequests, LeaseFrameFlyweight.numRequests(lease)); - Assertions.assertEquals(metadata, LeaseFrameFlyweight.metadata(lease)); - lease.release(); - } - - @Test - void leaseAbsentMetadata() { - int ttl = 1; - int numRequests = 42; - ByteBuf lease = LeaseFrameFlyweight.encode(ByteBufAllocator.DEFAULT, ttl, numRequests, null); - - Assertions.assertFalse(FrameHeaderFlyweight.hasMetadata(lease)); - Assertions.assertEquals(ttl, LeaseFrameFlyweight.ttl(lease)); - Assertions.assertEquals(numRequests, LeaseFrameFlyweight.numRequests(lease)); - Assertions.assertEquals(0, LeaseFrameFlyweight.metadata(lease).readableBytes()); - lease.release(); - } - - private static ByteBuf bytebuf(String str) { - return Unpooled.copiedBuffer(str, StandardCharsets.UTF_8); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/PayloadFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/PayloadFlyweightTest.java index 439d23c15..aecbb31ce 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/PayloadFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/PayloadFlyweightTest.java @@ -15,10 +15,9 @@ public class PayloadFlyweightTest { void nextCompleteDataMetadata() { Payload payload = DefaultPayload.create("d", "md"); ByteBuf nextComplete = - PayloadFrameFlyweight.encodeNextCompleteReleasingPayload( - ByteBufAllocator.DEFAULT, 1, payload); - String data = PayloadFrameFlyweight.data(nextComplete).toString(StandardCharsets.UTF_8); - String metadata = PayloadFrameFlyweight.metadata(nextComplete).toString(StandardCharsets.UTF_8); + PayloadFrameCodec.encodeNextCompleteReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); + String data = PayloadFrameCodec.data(nextComplete).toString(StandardCharsets.UTF_8); + String metadata = PayloadFrameCodec.metadata(nextComplete).toString(StandardCharsets.UTF_8); Assertions.assertEquals("d", data); Assertions.assertEquals("md", metadata); nextComplete.release(); @@ -28,10 +27,9 @@ void nextCompleteDataMetadata() { void nextCompleteData() { Payload payload = DefaultPayload.create("d"); ByteBuf nextComplete = - PayloadFrameFlyweight.encodeNextCompleteReleasingPayload( - ByteBufAllocator.DEFAULT, 1, payload); - String data = PayloadFrameFlyweight.data(nextComplete).toString(StandardCharsets.UTF_8); - ByteBuf metadata = PayloadFrameFlyweight.metadata(nextComplete); + PayloadFrameCodec.encodeNextCompleteReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); + String data = PayloadFrameCodec.data(nextComplete).toString(StandardCharsets.UTF_8); + ByteBuf metadata = PayloadFrameCodec.metadata(nextComplete); Assertions.assertEquals("d", data); Assertions.assertNull(metadata); nextComplete.release(); @@ -44,10 +42,9 @@ void nextCompleteMetaData() { Unpooled.EMPTY_BUFFER, Unpooled.wrappedBuffer("md".getBytes(StandardCharsets.UTF_8))); ByteBuf nextComplete = - PayloadFrameFlyweight.encodeNextCompleteReleasingPayload( - ByteBufAllocator.DEFAULT, 1, payload); - ByteBuf data = PayloadFrameFlyweight.data(nextComplete); - String metadata = PayloadFrameFlyweight.metadata(nextComplete).toString(StandardCharsets.UTF_8); + PayloadFrameCodec.encodeNextCompleteReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); + ByteBuf data = PayloadFrameCodec.data(nextComplete); + String metadata = PayloadFrameCodec.metadata(nextComplete).toString(StandardCharsets.UTF_8); Assertions.assertTrue(data.readableBytes() == 0); Assertions.assertEquals("md", metadata); nextComplete.release(); @@ -57,9 +54,9 @@ void nextCompleteMetaData() { void nextDataMetadata() { Payload payload = DefaultPayload.create("d", "md"); ByteBuf next = - PayloadFrameFlyweight.encodeNextReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); - String data = PayloadFrameFlyweight.data(next).toString(StandardCharsets.UTF_8); - String metadata = PayloadFrameFlyweight.metadata(next).toString(StandardCharsets.UTF_8); + PayloadFrameCodec.encodeNextReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); + String data = PayloadFrameCodec.data(next).toString(StandardCharsets.UTF_8); + String metadata = PayloadFrameCodec.metadata(next).toString(StandardCharsets.UTF_8); Assertions.assertEquals("d", data); Assertions.assertEquals("md", metadata); next.release(); @@ -69,9 +66,9 @@ void nextDataMetadata() { void nextData() { Payload payload = DefaultPayload.create("d"); ByteBuf next = - PayloadFrameFlyweight.encodeNextReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); - String data = PayloadFrameFlyweight.data(next).toString(StandardCharsets.UTF_8); - ByteBuf metadata = PayloadFrameFlyweight.metadata(next); + PayloadFrameCodec.encodeNextReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); + String data = PayloadFrameCodec.data(next).toString(StandardCharsets.UTF_8); + ByteBuf metadata = PayloadFrameCodec.metadata(next); Assertions.assertEquals("d", data); Assertions.assertNull(metadata); next.release(); @@ -81,9 +78,9 @@ void nextData() { void nextDataEmptyMetadata() { Payload payload = DefaultPayload.create("d".getBytes(), new byte[0]); ByteBuf next = - PayloadFrameFlyweight.encodeNextReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); - String data = PayloadFrameFlyweight.data(next).toString(StandardCharsets.UTF_8); - ByteBuf metadata = PayloadFrameFlyweight.metadata(next); + PayloadFrameCodec.encodeNextReleasingPayload(ByteBufAllocator.DEFAULT, 1, payload); + String data = PayloadFrameCodec.data(next).toString(StandardCharsets.UTF_8); + ByteBuf metadata = PayloadFrameCodec.metadata(next); Assertions.assertEquals("d", data); Assertions.assertEquals(metadata.readableBytes(), 0); next.release(); diff --git a/rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameCodecTest.java similarity index 63% rename from rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameFlyweightTest.java rename to rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameCodecTest.java index 4411b98c9..e38258040 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/RequestNFrameCodecTest.java @@ -7,12 +7,12 @@ import io.netty.buffer.ByteBufUtil; import org.junit.jupiter.api.Test; -class RequestNFrameFlyweightTest { +class RequestNFrameCodecTest { @Test void testEncoding() { - ByteBuf frame = RequestNFrameFlyweight.encode(ByteBufAllocator.DEFAULT, 1, 5); + ByteBuf frame = RequestNFrameCodec.encode(ByteBufAllocator.DEFAULT, 1, 5); - frame = FrameLengthFlyweight.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); + frame = FrameLengthCodec.encode(ByteBufAllocator.DEFAULT, frame.readableBytes(), frame); assertEquals("00000a00000001200000000005", ByteBufUtil.hexDump(frame)); frame.release(); } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameCodecTest.java similarity index 68% rename from rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameFlyweightTest.java rename to rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameCodecTest.java index f8b481f05..fe05335d2 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/ResumeFrameCodecTest.java @@ -23,19 +23,18 @@ import org.junit.Assert; import org.junit.jupiter.api.Test; -public class ResumeFrameFlyweightTest { +public class ResumeFrameCodecTest { @Test void testEncoding() { byte[] tokenBytes = new byte[65000]; Arrays.fill(tokenBytes, (byte) 1); ByteBuf token = Unpooled.wrappedBuffer(tokenBytes); - ByteBuf byteBuf = ResumeFrameFlyweight.encode(ByteBufAllocator.DEFAULT, token, 21, 12); - Assert.assertEquals( - ResumeFrameFlyweight.CURRENT_VERSION, ResumeFrameFlyweight.version(byteBuf)); - Assert.assertEquals(token, ResumeFrameFlyweight.token(byteBuf)); - Assert.assertEquals(21, ResumeFrameFlyweight.lastReceivedServerPos(byteBuf)); - Assert.assertEquals(12, ResumeFrameFlyweight.firstAvailableClientPos(byteBuf)); + ByteBuf byteBuf = ResumeFrameCodec.encode(ByteBufAllocator.DEFAULT, token, 21, 12); + Assert.assertEquals(ResumeFrameCodec.CURRENT_VERSION, ResumeFrameCodec.version(byteBuf)); + Assert.assertEquals(token, ResumeFrameCodec.token(byteBuf)); + Assert.assertEquals(21, ResumeFrameCodec.lastReceivedServerPos(byteBuf)); + Assert.assertEquals(12, ResumeFrameCodec.firstAvailableClientPos(byteBuf)); byteBuf.release(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameCodecTest.java similarity index 51% rename from rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameFlyweightTest.java rename to rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameCodecTest.java index c73409ffa..33dd8eb70 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/ResumeOkFrameCodecTest.java @@ -5,12 +5,12 @@ import org.junit.Assert; import org.junit.Test; -public class ResumeOkFrameFlyweightTest { +public class ResumeOkFrameCodecTest { @Test public void testEncoding() { - ByteBuf byteBuf = ResumeOkFrameFlyweight.encode(ByteBufAllocator.DEFAULT, 42); - Assert.assertEquals(42, ResumeOkFrameFlyweight.lastReceivedClientPos(byteBuf)); + ByteBuf byteBuf = ResumeOkFrameCodec.encode(ByteBufAllocator.DEFAULT, 42); + Assert.assertEquals(42, ResumeOkFrameCodec.lastReceivedClientPos(byteBuf)); byteBuf.release(); } } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameCodecTest.java new file mode 100644 index 000000000..f7d649972 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameCodecTest.java @@ -0,0 +1,57 @@ +package io.rsocket.frame; + +import static org.junit.jupiter.api.Assertions.*; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.buffer.Unpooled; +import io.rsocket.Payload; +import io.rsocket.util.DefaultPayload; +import java.util.Arrays; +import org.junit.jupiter.api.Test; + +class SetupFrameCodecTest { + @Test + void testEncodingNoResume() { + ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); + ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); + Payload payload = DefaultPayload.create(data, metadata); + ByteBuf frame = + SetupFrameCodec.encode( + ByteBufAllocator.DEFAULT, false, 5, 500, "metadata_type", "data_type", payload); + + assertEquals(FrameType.SETUP, FrameHeaderCodec.frameType(frame)); + assertFalse(SetupFrameCodec.resumeEnabled(frame)); + assertNull(SetupFrameCodec.resumeToken(frame)); + assertEquals("metadata_type", SetupFrameCodec.metadataMimeType(frame)); + assertEquals("data_type", SetupFrameCodec.dataMimeType(frame)); + assertEquals(metadata, SetupFrameCodec.metadata(frame)); + assertEquals(data, SetupFrameCodec.data(frame)); + assertEquals(SetupFrameCodec.CURRENT_VERSION, SetupFrameCodec.version(frame)); + frame.release(); + } + + @Test + void testEncodingResume() { + byte[] tokenBytes = new byte[65000]; + Arrays.fill(tokenBytes, (byte) 1); + ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); + ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); + Payload payload = DefaultPayload.create(data, metadata); + ByteBuf token = Unpooled.wrappedBuffer(tokenBytes); + ByteBuf frame = + SetupFrameCodec.encode( + ByteBufAllocator.DEFAULT, true, 5, 500, token, "metadata_type", "data_type", payload); + + assertEquals(FrameType.SETUP, FrameHeaderCodec.frameType(frame)); + assertTrue(SetupFrameCodec.honorLease(frame)); + assertTrue(SetupFrameCodec.resumeEnabled(frame)); + assertEquals(token, SetupFrameCodec.resumeToken(frame)); + assertEquals("metadata_type", SetupFrameCodec.metadataMimeType(frame)); + assertEquals("data_type", SetupFrameCodec.dataMimeType(frame)); + assertEquals(metadata, SetupFrameCodec.metadata(frame)); + assertEquals(data, SetupFrameCodec.data(frame)); + assertEquals(SetupFrameCodec.CURRENT_VERSION, SetupFrameCodec.version(frame)); + frame.release(); + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java deleted file mode 100644 index 128b3ff84..000000000 --- a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameFlyweightTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package io.rsocket.frame; - -import static org.junit.jupiter.api.Assertions.*; - -import io.netty.buffer.ByteBuf; -import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import io.rsocket.Payload; -import io.rsocket.util.DefaultPayload; -import java.util.Arrays; -import org.junit.jupiter.api.Test; - -class SetupFrameFlyweightTest { - @Test - void testEncodingNoResume() { - ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); - ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); - Payload payload = DefaultPayload.create(data, metadata); - ByteBuf frame = - SetupFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, false, 5, 500, "metadata_type", "data_type", payload); - - assertEquals(FrameType.SETUP, FrameHeaderFlyweight.frameType(frame)); - assertFalse(SetupFrameFlyweight.resumeEnabled(frame)); - assertNull(SetupFrameFlyweight.resumeToken(frame)); - assertEquals("metadata_type", SetupFrameFlyweight.metadataMimeType(frame)); - assertEquals("data_type", SetupFrameFlyweight.dataMimeType(frame)); - assertEquals(metadata, SetupFrameFlyweight.metadata(frame)); - assertEquals(data, SetupFrameFlyweight.data(frame)); - assertEquals(SetupFrameFlyweight.CURRENT_VERSION, SetupFrameFlyweight.version(frame)); - frame.release(); - } - - @Test - void testEncodingResume() { - byte[] tokenBytes = new byte[65000]; - Arrays.fill(tokenBytes, (byte) 1); - ByteBuf metadata = Unpooled.wrappedBuffer(new byte[] {1, 2, 3, 4}); - ByteBuf data = Unpooled.wrappedBuffer(new byte[] {5, 4, 3}); - Payload payload = DefaultPayload.create(data, metadata); - ByteBuf token = Unpooled.wrappedBuffer(tokenBytes); - ByteBuf frame = - SetupFrameFlyweight.encode( - ByteBufAllocator.DEFAULT, true, 5, 500, token, "metadata_type", "data_type", payload); - - assertEquals(FrameType.SETUP, FrameHeaderFlyweight.frameType(frame)); - assertTrue(SetupFrameFlyweight.honorLease(frame)); - assertTrue(SetupFrameFlyweight.resumeEnabled(frame)); - assertEquals(token, SetupFrameFlyweight.resumeToken(frame)); - assertEquals("metadata_type", SetupFrameFlyweight.metadataMimeType(frame)); - assertEquals("data_type", SetupFrameFlyweight.dataMimeType(frame)); - assertEquals(metadata, SetupFrameFlyweight.metadata(frame)); - assertEquals(data, SetupFrameFlyweight.data(frame)); - assertEquals(SetupFrameFlyweight.CURRENT_VERSION, SetupFrameFlyweight.version(frame)); - frame.release(); - } -} diff --git a/rsocket-core/src/test/java/io/rsocket/frame/VersionFlyweightTest.java b/rsocket-core/src/test/java/io/rsocket/frame/VersionCodecTest.java similarity index 58% rename from rsocket-core/src/test/java/io/rsocket/frame/VersionFlyweightTest.java rename to rsocket-core/src/test/java/io/rsocket/frame/VersionCodecTest.java index 3f311c7ef..be7fb837b 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/VersionFlyweightTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/VersionCodecTest.java @@ -20,29 +20,29 @@ import org.junit.jupiter.api.Test; -public class VersionFlyweightTest { +public class VersionCodecTest { @Test public void simple() { - int version = VersionFlyweight.encode(1, 0); - assertEquals(1, VersionFlyweight.major(version)); - assertEquals(0, VersionFlyweight.minor(version)); + int version = VersionCodec.encode(1, 0); + assertEquals(1, VersionCodec.major(version)); + assertEquals(0, VersionCodec.minor(version)); assertEquals(0x00010000, version); - assertEquals("1.0", VersionFlyweight.toString(version)); + assertEquals("1.0", VersionCodec.toString(version)); } @Test public void complex() { - int version = VersionFlyweight.encode(0x1234, 0x5678); - assertEquals(0x1234, VersionFlyweight.major(version)); - assertEquals(0x5678, VersionFlyweight.minor(version)); + int version = VersionCodec.encode(0x1234, 0x5678); + assertEquals(0x1234, VersionCodec.major(version)); + assertEquals(0x5678, VersionCodec.minor(version)); assertEquals(0x12345678, version); - assertEquals("4660.22136", VersionFlyweight.toString(version)); + assertEquals("4660.22136", VersionCodec.toString(version)); } @Test public void noShortOverflow() { - int version = VersionFlyweight.encode(43210, 43211); - assertEquals(43210, VersionFlyweight.major(version)); - assertEquals(43211, VersionFlyweight.minor(version)); + int version = VersionCodec.encode(43210, 43211); + assertEquals(43210, VersionCodec.major(version)); + assertEquals(43211, VersionCodec.minor(version)); } } diff --git a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java index efa962c48..63acc40aa 100644 --- a/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java +++ b/rsocket-core/src/test/java/io/rsocket/internal/ClientServerInputMultiplexerTest.java @@ -193,11 +193,11 @@ public void serverSplits() { } private ByteBuf resumeFrame() { - return ResumeFrameFlyweight.encode(allocator, Unpooled.EMPTY_BUFFER, 0, 0); + return ResumeFrameCodec.encode(allocator, Unpooled.EMPTY_BUFFER, 0, 0); } private ByteBuf setupFrame() { - return SetupFrameFlyweight.encode( + return SetupFrameCodec.encode( ByteBufAllocator.DEFAULT, false, 0, @@ -208,22 +208,22 @@ private ByteBuf setupFrame() { } private ByteBuf leaseFrame() { - return LeaseFrameFlyweight.encode(allocator, 1_000, 1, Unpooled.EMPTY_BUFFER); + return LeaseFrameCodec.encode(allocator, 1_000, 1, Unpooled.EMPTY_BUFFER); } private ByteBuf errorFrame(int i) { - return ErrorFrameFlyweight.encode(allocator, i, new Exception()); + return ErrorFrameCodec.encode(allocator, i, new Exception()); } private ByteBuf resumeOkFrame() { - return ResumeOkFrameFlyweight.encode(allocator, 0); + return ResumeOkFrameCodec.encode(allocator, 0); } private ByteBuf keepAliveFrame() { - return KeepAliveFrameFlyweight.encode(allocator, false, 0, Unpooled.EMPTY_BUFFER); + return KeepAliveFrameCodec.encode(allocator, false, 0, Unpooled.EMPTY_BUFFER); } private ByteBuf metadataPushFrame() { - return MetadataPushFrameFlyweight.encode(allocator, Unpooled.EMPTY_BUFFER); + return MetadataPushFrameCodec.encode(allocator, Unpooled.EMPTY_BUFFER); } } diff --git a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java index 9904c2b24..c8b22382a 100644 --- a/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java +++ b/rsocket-micrometer/src/main/java/io/rsocket/micrometer/MicrometerDuplexConnection.java @@ -22,7 +22,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; -import io.rsocket.frame.FrameHeaderFlyweight; +import io.rsocket.frame.FrameHeaderCodec; import io.rsocket.frame.FrameType; import io.rsocket.plugins.DuplexConnectionInterceptor.Type; import java.util.Objects; @@ -191,7 +191,7 @@ private static Counter counter( @Override public void accept(ByteBuf frame) { - FrameType frameType = FrameHeaderFlyweight.frameType(frame); + FrameType frameType = FrameHeaderCodec.frameType(frame); switch (frameType) { case SETUP: diff --git a/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java index 60ff05124..1e66abc5e 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TestFrames.java @@ -33,71 +33,69 @@ private TestFrames() {} /** @return {@link ByteBuf} representing test instance of Cancel frame */ public static ByteBuf createTestCancelFrame() { - return CancelFrameFlyweight.encode(allocator, 1); + return CancelFrameCodec.encode(allocator, 1); } /** @return {@link ByteBuf} representing test instance of Error frame */ public static ByteBuf createTestErrorFrame() { - return ErrorFrameFlyweight.encode(allocator, 1, new RuntimeException()); + return ErrorFrameCodec.encode(allocator, 1, new RuntimeException()); } /** @return {@link ByteBuf} representing test instance of Extension frame */ public static ByteBuf createTestExtensionFrame() { - return ExtensionFrameFlyweight.encode( + return ExtensionFrameCodec.encode( allocator, 1, 1, Unpooled.EMPTY_BUFFER, Unpooled.EMPTY_BUFFER); } /** @return {@link ByteBuf} representing test instance of Keep-Alive frame */ public static ByteBuf createTestKeepaliveFrame() { - return KeepAliveFrameFlyweight.encode(allocator, false, 1, Unpooled.EMPTY_BUFFER); + return KeepAliveFrameCodec.encode(allocator, false, 1, Unpooled.EMPTY_BUFFER); } /** @return {@link ByteBuf} representing test instance of Lease frame */ public static ByteBuf createTestLeaseFrame() { - return LeaseFrameFlyweight.encode(allocator, 1, 1, null); + return LeaseFrameCodec.encode(allocator, 1, 1, null); } /** @return {@link ByteBuf} representing test instance of Metadata-Push frame */ public static ByteBuf createTestMetadataPushFrame() { - return MetadataPushFrameFlyweight.encode(allocator, Unpooled.EMPTY_BUFFER); + return MetadataPushFrameCodec.encode(allocator, Unpooled.EMPTY_BUFFER); } /** @return {@link ByteBuf} representing test instance of Payload frame */ public static ByteBuf createTestPayloadFrame() { - return PayloadFrameFlyweight.encode( - allocator, 1, false, true, false, null, Unpooled.EMPTY_BUFFER); + return PayloadFrameCodec.encode(allocator, 1, false, true, false, null, Unpooled.EMPTY_BUFFER); } /** @return {@link ByteBuf} representing test instance of Request-Channel frame */ public static ByteBuf createTestRequestChannelFrame() { - return RequestChannelFrameFlyweight.encode( + return RequestChannelFrameCodec.encode( allocator, 1, false, false, 1, null, Unpooled.EMPTY_BUFFER); } /** @return {@link ByteBuf} representing test instance of Fire-and-Forget frame */ public static ByteBuf createTestRequestFireAndForgetFrame() { - return RequestFireAndForgetFrameFlyweight.encode( - allocator, 1, false, null, Unpooled.EMPTY_BUFFER); + return RequestFireAndForgetFrameCodec.encode(allocator, 1, false, null, Unpooled.EMPTY_BUFFER); } /** @return {@link ByteBuf} representing test instance of Request-N frame */ public static ByteBuf createTestRequestNFrame() { - return RequestNFrameFlyweight.encode(allocator, 1, 1); + return RequestNFrameCodec.encode(allocator, 1, 1); } /** @return {@link ByteBuf} representing test instance of Request-Response frame */ public static ByteBuf createTestRequestResponseFrame() { - return RequestResponseFrameFlyweight.encodeReleasingPayload(allocator, 1, emptyPayload); + return RequestResponseFrameCodec.encodeReleasingPayload(allocator, 1, emptyPayload); } /** @return {@link ByteBuf} representing test instance of Request-Stream frame */ public static ByteBuf createTestRequestStreamFrame() { - return RequestStreamFrameFlyweight.encodeReleasingPayload(allocator, 1, 1L, emptyPayload); + return RequestStreamFrameCodec.encodeReleasingPayload(allocator, 1, 1L, emptyPayload); } /** @return {@link ByteBuf} representing test instance of Setup frame */ public static ByteBuf createTestSetupFrame() { - return SetupFrameFlyweight.encode( + return SetupFrameCodec.encode( allocator, false, 1, diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/RSocketLengthCodec.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/RSocketLengthCodec.java index 68d7ab175..e2c134653 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/RSocketLengthCodec.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/RSocketLengthCodec.java @@ -16,8 +16,8 @@ package io.rsocket.transport.netty; -import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; -import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_SIZE; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_SIZE; import io.netty.buffer.ByteBuf; import io.netty.channel.ChannelHandlerContext; diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java index d71d6b356..b7081593c 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/TcpDuplexConnection.java @@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.DuplexConnection; -import io.rsocket.frame.FrameLengthFlyweight; +import io.rsocket.frame.FrameLengthCodec; import io.rsocket.internal.BaseDuplexConnection; import java.util.Objects; import org.reactivestreams.Publisher; @@ -88,7 +88,7 @@ public Mono send(Publisher frames) { private ByteBuf encode(ByteBuf frame) { if (encodeLength) { - return FrameLengthFlyweight.encode(alloc(), frame.readableBytes(), frame); + return FrameLengthCodec.encode(alloc(), frame.readableBytes(), frame); } else { return frame; } @@ -96,7 +96,7 @@ private ByteBuf encode(ByteBuf frame) { private ByteBuf decode(ByteBuf frame) { if (encodeLength) { - return FrameLengthFlyweight.frame(frame).retain(); + return FrameLengthCodec.frame(frame).retain(); } else { return frame; } 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 b19621d46..6991728ca 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 @@ -16,7 +16,7 @@ package io.rsocket.transport.netty.client; -import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; import static io.rsocket.transport.netty.UriUtils.getPort; import static io.rsocket.transport.netty.UriUtils.isSecure; diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java index 83cb010b7..bd19f18b0 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketRouteTransport.java @@ -16,7 +16,7 @@ package io.rsocket.transport.netty.server; -import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; import io.rsocket.Closeable; import io.rsocket.DuplexConnection; diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java index 4a0331c08..1a0b32cf0 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/server/WebsocketServerTransport.java @@ -16,7 +16,7 @@ package io.rsocket.transport.netty.server; -import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; import io.rsocket.DuplexConnection; import io.rsocket.fragmentation.FragmentationDuplexConnection; diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java index 905f022f2..fc035c536 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/client/WebsocketClientTransportTest.java @@ -16,7 +16,7 @@ package io.rsocket.transport.netty.client; -import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java index 5a2986485..249a3e12a 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/server/WebsocketServerTransportTest.java @@ -16,7 +16,7 @@ package io.rsocket.transport.netty.server; -import static io.rsocket.frame.FrameLengthFlyweight.FRAME_LENGTH_MASK; +import static io.rsocket.frame.FrameLengthCodec.FRAME_LENGTH_MASK; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatNullPointerException; From cec7a789c7a644c20b1c48815e5446c8099a8d13 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Fri, 8 May 2020 20:00:56 +0300 Subject: [PATCH 172/181] provides extra @NonNullApi annotation for all packages (#826) --- build.gradle | 1 - rsocket-core/build.gradle | 2 -- .../io/rsocket/ConnectionSetupPayload.java | 2 +- .../io/rsocket/core/RSocketConnector.java | 2 +- .../io/rsocket/core/RSocketRequester.java | 2 +- .../io/rsocket/core/RSocketResponder.java | 2 +- .../io/rsocket/core/StreamIdSupplier.java | 1 + .../exceptions/ApplicationErrorException.java | 2 +- .../rsocket/exceptions/CanceledException.java | 2 +- .../exceptions/ConnectionCloseException.java | 2 +- .../exceptions/ConnectionErrorException.java | 2 +- .../exceptions/CustomRSocketException.java | 2 +- .../rsocket/exceptions/InvalidException.java | 2 +- .../exceptions/InvalidSetupException.java | 2 +- .../rsocket/exceptions/RejectedException.java | 2 +- .../exceptions/RejectedResumeException.java | 2 +- .../exceptions/RejectedSetupException.java | 2 +- .../io/rsocket/exceptions/SetupException.java | 2 +- .../exceptions/UnsupportedSetupException.java | 2 +- .../FragmentationDuplexConnection.java | 2 +- .../fragmentation/FrameReassembler.java | 8 +++++-- .../io/rsocket/frame/ExtensionFrameCodec.java | 3 ++- .../java/io/rsocket/frame/FrameBodyCodec.java | 5 ++-- .../io/rsocket/frame/GenericFrameCodec.java | 6 +++-- .../io/rsocket/frame/LeaseFrameCodec.java | 6 ++--- .../rsocket/frame/MetadataPushFrameCodec.java | 4 ++++ .../io/rsocket/frame/PayloadFrameCodec.java | 6 +++-- .../frame/RequestChannelFrameCodec.java | 4 +++- .../frame/RequestFireAndForgetFrameCodec.java | 4 +++- .../frame/RequestResponseFrameCodec.java | 4 +++- .../frame/RequestStreamFrameCodec.java | 4 +++- .../io/rsocket/frame/SetupFrameCodec.java | 6 +++-- .../rsocket/frame/decoder/package-info.java | 24 +++++++++++++++++++ .../java/io/rsocket/frame/package-info.java | 3 +++ .../ClientServerInputMultiplexer.java | 1 + .../io/rsocket/internal/package-info.java | 4 +++- .../io/rsocket/keepalive/package-info.java | 4 +++- .../src/main/java/io/rsocket/lease/Lease.java | 4 +--- .../main/java/io/rsocket/lease/LeaseImpl.java | 4 +--- .../rsocket/lease/MissingLeaseException.java | 7 +++--- .../rsocket/lease/ResponderLeaseHandler.java | 2 +- .../java/io/rsocket/lease/package-info.java | 4 +++- .../io/rsocket/metadata/package-info.java | 3 +++ .../main/java/io/rsocket/package-info.java | 3 +++ .../java/io/rsocket/plugins/package-info.java | 3 +++ .../io/rsocket/resume/SessionManager.java | 2 +- .../java/io/rsocket/resume/package-info.java | 4 +++- .../io/rsocket/transport/package-info.java | 4 +++- .../java/io/rsocket/util/ByteBufPayload.java | 2 +- .../java/io/rsocket/util/DefaultPayload.java | 2 +- .../java/io/rsocket/util/package-info.java | 4 +++- .../core/ConnectionSetupPayloadTest.java | 2 +- .../core/RSocketRequesterSubscribersTest.java | 2 +- .../io/rsocket/frame/LeaseFrameCodecTest.java | 2 +- .../io/rsocket/frame/SetupFrameCodecTest.java | 2 +- .../rsocket/client/filter/package-info.java | 20 ++++++++++++++++ .../java/io/rsocket/client/package-info.java | 20 ++++++++++++++++ .../java/io/rsocket/stat/package-info.java | 20 ++++++++++++++++ rsocket-micrometer/build.gradle | 2 -- rsocket-test/build.gradle | 2 -- rsocket-transport-local/build.gradle | 2 -- rsocket-transport-netty/build.gradle | 2 -- .../netty/client/TcpClientTransport.java | 2 +- .../client/WebsocketClientTransport.java | 8 ++++--- .../netty/server/CloseableChannel.java | 2 +- .../netty/TcpSecureTransportTest.java | 4 ++-- .../netty/WebsocketSecureTransportTest.java | 4 ++-- 67 files changed, 199 insertions(+), 79 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/frame/decoder/package-info.java create mode 100644 rsocket-load-balancer/src/main/java/io/rsocket/client/filter/package-info.java create mode 100644 rsocket-load-balancer/src/main/java/io/rsocket/client/package-info.java create mode 100644 rsocket-load-balancer/src/main/java/io/rsocket/stat/package-info.java diff --git a/build.gradle b/build.gradle index 2c7757e0f..f579b3ae0 100644 --- a/build.gradle +++ b/build.gradle @@ -62,7 +62,6 @@ subprojects { dependencies { dependency "ch.qos.logback:logback-classic:${ext['logback.version']}" - dependency "com.google.code.findbugs:jsr305:${ext['findbugs.version']}" dependency "io.netty:netty-tcnative-boringssl-static:${ext['netty-boringssl.version']}" dependency "io.micrometer:micrometer-core:${ext['micrometer.version']}" dependency "org.assertj:assertj-core:${ext['assertj.version']}" diff --git a/rsocket-core/build.gradle b/rsocket-core/build.gradle index ca2ae0e65..41adbd7a8 100644 --- a/rsocket-core/build.gradle +++ b/rsocket-core/build.gradle @@ -29,8 +29,6 @@ dependencies { implementation 'org.slf4j:slf4j-api' - compileOnly 'com.google.code.findbugs:jsr305' - testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' testImplementation 'org.junit.jupiter:junit-jupiter-api' diff --git a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java index bd4582e2b..ece2aa9fa 100644 --- a/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/ConnectionSetupPayload.java @@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.util.AbstractReferenceCounted; import io.rsocket.core.DefaultConnectionSetupPayload; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** * Exposes information from the {@code SETUP} frame to a server, as well as to client responders. diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index b69610f3f..38393c27d 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -41,10 +41,10 @@ import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; -import javax.annotation.Nullable; import reactor.core.Disposable; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; +import reactor.util.annotation.Nullable; import reactor.util.retry.Retry; /** diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 846eaa922..068797d39 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -55,7 +55,6 @@ import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; import java.util.function.Supplier; -import javax.annotation.Nullable; import org.reactivestreams.Processor; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -68,6 +67,7 @@ import reactor.core.publisher.SignalType; import reactor.core.publisher.UnicastProcessor; import reactor.core.scheduler.Scheduler; +import reactor.util.annotation.Nullable; import reactor.util.concurrent.Queues; /** diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index b9d3ea794..dce182b49 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -39,7 +39,6 @@ import java.util.function.Consumer; import java.util.function.LongConsumer; import java.util.function.Supplier; -import javax.annotation.Nullable; import org.reactivestreams.Processor; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; @@ -47,6 +46,7 @@ import reactor.core.Disposable; import reactor.core.Exceptions; import reactor.core.publisher.*; +import reactor.util.annotation.Nullable; import reactor.util.concurrent.Queues; /** Responder side of RSocket. Receives {@link ByteBuf}s from a peer's {@link RSocketRequester} */ diff --git a/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java b/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java index 7f4d7b7b3..15d39c993 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java +++ b/rsocket-core/src/main/java/io/rsocket/core/StreamIdSupplier.java @@ -17,6 +17,7 @@ import io.netty.util.collection.IntObjectMap; +/** This API is not thread-safe and must be strictly used in serialized fashion */ final class StreamIdSupplier { private static final int MASK = 0x7FFFFFFF; diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java index e617b82d8..cd0d46754 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ApplicationErrorException.java @@ -17,7 +17,7 @@ package io.rsocket.exceptions; import io.rsocket.frame.ErrorFrameCodec; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** * Application layer logic generating a Reactive Streams {@code onError} event. diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java index 3c5fc7420..d51ba0fb7 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/CanceledException.java @@ -17,7 +17,7 @@ package io.rsocket.exceptions; import io.rsocket.frame.ErrorFrameCodec; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** * The Responder canceled the request but may have started processing it (similar to REJECTED but diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java index 5cff2c821..80324aa90 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionCloseException.java @@ -17,7 +17,7 @@ package io.rsocket.exceptions; import io.rsocket.frame.ErrorFrameCodec; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** * The connection is being terminated. Sender or Receiver of this frame MUST wait for outstanding diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java index 3fcb8f5de..b44714f7e 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/ConnectionErrorException.java @@ -17,7 +17,7 @@ package io.rsocket.exceptions; import io.rsocket.frame.ErrorFrameCodec; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** * The connection is being terminated. Sender or Receiver of this frame MAY close the connection diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java index 18f488ba0..079b561f9 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/CustomRSocketException.java @@ -17,7 +17,7 @@ package io.rsocket.exceptions; import io.rsocket.frame.ErrorFrameCodec; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; public class CustomRSocketException extends RSocketException { private static final long serialVersionUID = 7873267740343446585L; diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java index 2428d1e7e..a1b77b8dd 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidException.java @@ -17,7 +17,7 @@ package io.rsocket.exceptions; import io.rsocket.frame.ErrorFrameCodec; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** * The request is invalid. diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java index 57da19bb6..b0889c5a6 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/InvalidSetupException.java @@ -17,7 +17,7 @@ package io.rsocket.exceptions; import io.rsocket.frame.ErrorFrameCodec; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** * The Setup frame is invalid for the server (it could be that the client is too recent for the old diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java index c87a60243..baed84e1b 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedException.java @@ -17,7 +17,7 @@ package io.rsocket.exceptions; import io.rsocket.frame.ErrorFrameCodec; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** * Despite being a valid request, the Responder decided to reject it. The Responder guarantees that diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java index 8a6ea2244..8a99fcffb 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedResumeException.java @@ -17,7 +17,7 @@ package io.rsocket.exceptions; import io.rsocket.frame.ErrorFrameCodec; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** * The server rejected the resume, it can specify the reason in the payload. diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java index 972b430a7..c09a27e32 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/RejectedSetupException.java @@ -17,7 +17,7 @@ package io.rsocket.exceptions; import io.rsocket.frame.ErrorFrameCodec; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** * The server rejected the setup, it can specify the reason in the payload. diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java index 158e5410d..ed979c9e6 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/SetupException.java @@ -17,7 +17,7 @@ package io.rsocket.exceptions; import io.rsocket.frame.ErrorFrameCodec; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** The root of the setup exception hierarchy. */ public abstract class SetupException extends RSocketException { diff --git a/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java b/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java index 3282c9750..7429ccd98 100644 --- a/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java +++ b/rsocket-core/src/main/java/io/rsocket/exceptions/UnsupportedSetupException.java @@ -17,7 +17,7 @@ package io.rsocket.exceptions; import io.rsocket.frame.ErrorFrameCodec; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** * Some (or all) of the parameters specified by the client are unsupported by the server. diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java index 5192ffead..5d89bb9ad 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FragmentationDuplexConnection.java @@ -25,12 +25,12 @@ import io.rsocket.frame.FrameLengthCodec; import io.rsocket.frame.FrameType; import java.util.Objects; -import javax.annotation.Nullable; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import reactor.util.annotation.Nullable; /** * A {@link DuplexConnection} implementation that fragments and reassembles {@link ByteBuf}s. diff --git a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java index 1e96bd1fc..52068e5de 100644 --- a/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java +++ b/rsocket-core/src/main/java/io/rsocket/fragmentation/FrameReassembler.java @@ -29,6 +29,7 @@ import org.slf4j.LoggerFactory; import reactor.core.Disposable; import reactor.core.publisher.SynchronousSink; +import reactor.util.annotation.Nullable; /** * The implementation of the RSocket reassembly behavior. @@ -83,6 +84,7 @@ public boolean isDisposed() { return get(); } + @Nullable synchronized ByteBuf getHeader(int streamId) { return headers.get(streamId); } @@ -109,14 +111,17 @@ synchronized CompositeByteBuf getData(int streamId) { return byteBuf; } + @Nullable synchronized ByteBuf removeHeader(int streamId) { return headers.remove(streamId); } + @Nullable synchronized CompositeByteBuf removeMetadata(int streamId) { return metadata.remove(streamId); } + @Nullable synchronized CompositeByteBuf removeData(int streamId) { return data.remove(streamId); } @@ -236,7 +241,6 @@ void reassembleFrame(ByteBuf frame, SynchronousSink sink) { case CANCEL: case ERROR: cancelAssemble(streamId); - default: } if (!frameType.isFragmentable()) { @@ -270,7 +274,7 @@ private ByteBuf assembleFrameWithMetadata(ByteBuf frame, int streamId, ByteBuf h metadata = PayloadFrameCodec.metadata(frame).retain(); } } else { - metadata = cm != null ? cm : null; + metadata = cm; } ByteBuf data = assembleData(frame, streamId); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameCodec.java index bf30b9556..418926596 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/ExtensionFrameCodec.java @@ -2,7 +2,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; public class ExtensionFrameCodec { private ExtensionFrameCodec() {} @@ -49,6 +49,7 @@ public static ByteBuf data(ByteBuf byteBuf) { return data; } + @Nullable public static ByteBuf metadata(ByteBuf byteBuf) { FrameHeaderCodec.ensureFrameType(FrameType.EXT, byteBuf); diff --git a/rsocket-core/src/main/java/io/rsocket/frame/FrameBodyCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/FrameBodyCodec.java index 3256d4426..ea011e503 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/FrameBodyCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/FrameBodyCodec.java @@ -3,6 +3,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.Unpooled; +import reactor.util.annotation.Nullable; class FrameBodyCodec { public static final int FRAME_LENGTH_MASK = 0xFFFFFF; @@ -33,9 +34,9 @@ private static int decodeLength(final ByteBuf byteBuf) { static ByteBuf encode( ByteBufAllocator allocator, final ByteBuf header, - ByteBuf metadata, + @Nullable ByteBuf metadata, boolean hasMetadata, - ByteBuf data) { + @Nullable ByteBuf data) { final boolean addData; if (data != null) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/GenericFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/GenericFrameCodec.java index 65e7eeeea..56a93d869 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/GenericFrameCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/GenericFrameCodec.java @@ -4,7 +4,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.util.IllegalReferenceCountException; import io.rsocket.Payload; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; class GenericFrameCodec { @@ -75,7 +75,7 @@ static ByteBuf encode( boolean next, int requestN, @Nullable ByteBuf metadata, - ByteBuf data) { + @Nullable ByteBuf data) { final boolean hasMetadata = metadata != null; @@ -115,6 +115,7 @@ static ByteBuf data(ByteBuf byteBuf) { return data; } + @Nullable static ByteBuf metadata(ByteBuf byteBuf) { boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); if (!hasMetadata) { @@ -136,6 +137,7 @@ static ByteBuf dataWithRequestN(ByteBuf byteBuf) { return data; } + @Nullable static ByteBuf metadataWithRequestN(ByteBuf byteBuf) { boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); if (!hasMetadata) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameCodec.java index cafd80104..f20c25d3b 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/LeaseFrameCodec.java @@ -2,8 +2,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; -import io.netty.buffer.Unpooled; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; public class LeaseFrameCodec { @@ -67,6 +66,7 @@ public static int numRequests(final ByteBuf byteBuf) { return numRequests; } + @Nullable public static ByteBuf metadata(final ByteBuf byteBuf) { FrameHeaderCodec.ensureFrameType(FrameType.LEASE, byteBuf); if (FrameHeaderCodec.hasMetadata(byteBuf)) { @@ -77,7 +77,7 @@ public static ByteBuf metadata(final ByteBuf byteBuf) { byteBuf.resetReaderIndex(); return metadata; } else { - return Unpooled.EMPTY_BUFFER; + return null; } } } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameCodec.java index 62c8a17dc..d8ffe3eef 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/MetadataPushFrameCodec.java @@ -8,6 +8,10 @@ public class MetadataPushFrameCodec { public static ByteBuf encodeReleasingPayload(ByteBufAllocator allocator, Payload payload) { + if (!payload.hasMetadata()) { + throw new IllegalStateException( + "Metadata push requires to have metadata present" + " in the given Payload"); + } final ByteBuf metadata = payload.metadata().retain(); // releasing payload safely since it can be already released wheres we have to release retained // data and metadata as well diff --git a/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameCodec.java index 8a7e6427a..1ae9c6750 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/PayloadFrameCodec.java @@ -3,6 +3,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.Payload; +import reactor.util.annotation.Nullable; public class PayloadFrameCodec { @@ -37,8 +38,8 @@ public static ByteBuf encode( boolean fragmentFollows, boolean complete, boolean next, - ByteBuf metadata, - ByteBuf data) { + @Nullable ByteBuf metadata, + @Nullable ByteBuf data) { return GenericFrameCodec.encode( allocator, FrameType.PAYLOAD, streamId, fragmentFollows, complete, next, 0, metadata, data); @@ -48,6 +49,7 @@ public static ByteBuf data(ByteBuf byteBuf) { return GenericFrameCodec.data(byteBuf); } + @Nullable public static ByteBuf metadata(ByteBuf byteBuf) { return GenericFrameCodec.metadata(byteBuf); } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameCodec.java index 2ff887043..60906083d 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestChannelFrameCodec.java @@ -3,6 +3,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.Payload; +import reactor.util.annotation.Nullable; public class RequestChannelFrameCodec { @@ -31,7 +32,7 @@ public static ByteBuf encode( boolean fragmentFollows, boolean complete, long initialRequestN, - ByteBuf metadata, + @Nullable ByteBuf metadata, ByteBuf data) { if (initialRequestN < 1) { @@ -56,6 +57,7 @@ public static ByteBuf data(ByteBuf byteBuf) { return GenericFrameCodec.dataWithRequestN(byteBuf); } + @Nullable public static ByteBuf metadata(ByteBuf byteBuf) { return GenericFrameCodec.metadataWithRequestN(byteBuf); } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameCodec.java index ddb5bc5d7..b91199179 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestFireAndForgetFrameCodec.java @@ -3,6 +3,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.Payload; +import reactor.util.annotation.Nullable; public class RequestFireAndForgetFrameCodec { @@ -19,7 +20,7 @@ public static ByteBuf encode( ByteBufAllocator allocator, int streamId, boolean fragmentFollows, - ByteBuf metadata, + @Nullable ByteBuf metadata, ByteBuf data) { return GenericFrameCodec.encode( @@ -30,6 +31,7 @@ public static ByteBuf data(ByteBuf byteBuf) { return GenericFrameCodec.data(byteBuf); } + @Nullable public static ByteBuf metadata(ByteBuf byteBuf) { return GenericFrameCodec.metadata(byteBuf); } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameCodec.java index 884a8293b..4a37acfd5 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestResponseFrameCodec.java @@ -3,6 +3,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.Payload; +import reactor.util.annotation.Nullable; public class RequestResponseFrameCodec { @@ -19,7 +20,7 @@ public static ByteBuf encode( ByteBufAllocator allocator, int streamId, boolean fragmentFollows, - ByteBuf metadata, + @Nullable ByteBuf metadata, ByteBuf data) { return GenericFrameCodec.encode( allocator, FrameType.REQUEST_RESPONSE, streamId, fragmentFollows, metadata, data); @@ -29,6 +30,7 @@ public static ByteBuf data(ByteBuf byteBuf) { return GenericFrameCodec.data(byteBuf); } + @Nullable public static ByteBuf metadata(ByteBuf byteBuf) { return GenericFrameCodec.metadata(byteBuf); } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameCodec.java index 9853a8b54..2f5dbf0d8 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/RequestStreamFrameCodec.java @@ -3,6 +3,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.Payload; +import reactor.util.annotation.Nullable; public class RequestStreamFrameCodec { @@ -26,7 +27,7 @@ public static ByteBuf encode( int streamId, boolean fragmentFollows, long initialRequestN, - ByteBuf metadata, + @Nullable ByteBuf metadata, ByteBuf data) { if (initialRequestN < 1) { @@ -51,6 +52,7 @@ public static ByteBuf data(ByteBuf byteBuf) { return GenericFrameCodec.dataWithRequestN(byteBuf); } + @Nullable public static ByteBuf metadata(ByteBuf byteBuf) { return GenericFrameCodec.metadataWithRequestN(byteBuf); } diff --git a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameCodec.java b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameCodec.java index d6f7431e4..547e2436e 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/SetupFrameCodec.java @@ -6,6 +6,7 @@ import io.netty.buffer.Unpooled; import io.rsocket.Payload; import java.nio.charset.StandardCharsets; +import reactor.util.annotation.Nullable; public class SetupFrameCodec { /** @@ -61,7 +62,7 @@ public static ByteBuf encode( int flags = 0; - if (resumeToken != null && resumeToken.readableBytes() > 0) { + if (resumeToken.readableBytes() > 0) { flags |= FLAGS_RESUME_ENABLE; } @@ -163,7 +164,7 @@ public static ByteBuf resumeToken(ByteBuf byteBuf) { byteBuf.resetReaderIndex(); return resumeToken; } else { - return null; + return Unpooled.EMPTY_BUFFER; } } @@ -186,6 +187,7 @@ public static String dataMimeType(ByteBuf byteBuf) { return mimeType; } + @Nullable public static ByteBuf metadata(ByteBuf byteBuf) { boolean hasMetadata = FrameHeaderCodec.hasMetadata(byteBuf); if (!hasMetadata) { diff --git a/rsocket-core/src/main/java/io/rsocket/frame/decoder/package-info.java b/rsocket-core/src/main/java/io/rsocket/frame/decoder/package-info.java new file mode 100644 index 000000000..82e8acaf3 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/frame/decoder/package-info.java @@ -0,0 +1,24 @@ +/* + * 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. + */ + +/** + * Support for encoding and decoding of RSocket frames to and from {@link io.rsocket.Payload + * Payload}. + */ +@NonNullApi +package io.rsocket.frame.decoder; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/frame/package-info.java b/rsocket-core/src/main/java/io/rsocket/frame/package-info.java index 1d02ebca0..69f6d6860 100644 --- a/rsocket-core/src/main/java/io/rsocket/frame/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/frame/package-info.java @@ -18,4 +18,7 @@ * Support for encoding and decoding of RSocket frames to and from {@link io.rsocket.Payload * Payload}. */ +@NonNullApi package io.rsocket.frame; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index c294d6539..0d24c51d8 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -161,6 +161,7 @@ private static class InternalDuplexConnection implements DuplexConnection { private final MonoProcessor>[] processors; private final boolean debugEnabled; + @SafeVarargs public InternalDuplexConnection( DuplexConnection source, MonoProcessor>... processors) { this.source = source; diff --git a/rsocket-core/src/main/java/io/rsocket/internal/package-info.java b/rsocket-core/src/main/java/io/rsocket/internal/package-info.java index 09918f3d1..07ddfab41 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/package-info.java @@ -18,5 +18,7 @@ * Internal package and must not be used outside this project. There are no guarantees for * API compatibility. */ -@javax.annotation.ParametersAreNonnullByDefault +@NonNullApi package io.rsocket.internal; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/keepalive/package-info.java b/rsocket-core/src/main/java/io/rsocket/keepalive/package-info.java index ce8a2f3fb..d94a93cad 100644 --- a/rsocket-core/src/main/java/io/rsocket/keepalive/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/keepalive/package-info.java @@ -15,5 +15,7 @@ */ /** Support classes for sending and keeping track of KEEPALIVE frames from the remote. */ -@javax.annotation.ParametersAreNonnullByDefault +@NonNullApi package io.rsocket.keepalive; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/lease/Lease.java b/rsocket-core/src/main/java/io/rsocket/lease/Lease.java index b9d99f88a..673b4a480 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/Lease.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/Lease.java @@ -19,8 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.rsocket.Availability; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** A contract for RSocket lease, which is sent by a request acceptor and is time bound. */ public interface Lease extends Availability { @@ -78,7 +77,6 @@ default int getRemainingTimeToLiveMillis(long now) { * * @return Metadata for the lease. */ - @Nonnull ByteBuf getMetadata(); /** diff --git a/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java b/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java index 63b0433cb..7abb8aab9 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/LeaseImpl.java @@ -19,8 +19,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import java.util.concurrent.atomic.AtomicInteger; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; public class LeaseImpl implements Lease { private final int timeToLiveMillis; @@ -60,7 +59,6 @@ public int getStartingAllowedRequests() { return startingAllowedRequests; } - @Nonnull @Override public ByteBuf getMetadata() { return metadata; diff --git a/rsocket-core/src/main/java/io/rsocket/lease/MissingLeaseException.java b/rsocket-core/src/main/java/io/rsocket/lease/MissingLeaseException.java index 734d16d07..3b6cec62c 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/MissingLeaseException.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/MissingLeaseException.java @@ -17,17 +17,16 @@ import io.rsocket.exceptions.RejectedException; import java.util.Objects; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; public class MissingLeaseException extends RejectedException { private static final long serialVersionUID = -6169748673403858959L; - public MissingLeaseException(@Nonnull Lease lease, @Nonnull String tag) { + public MissingLeaseException(Lease lease, String tag) { super(leaseMessage(Objects.requireNonNull(lease), Objects.requireNonNull(tag))); } - public MissingLeaseException(@Nonnull String tag) { + public MissingLeaseException(String tag) { super(leaseMessage(null, Objects.requireNonNull(tag))); } diff --git a/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java b/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java index 5f000cb30..2035ade87 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java @@ -23,10 +23,10 @@ import java.util.Optional; import java.util.function.Consumer; import java.util.function.Function; -import javax.annotation.Nullable; import reactor.core.Disposable; import reactor.core.Disposables; import reactor.core.publisher.Flux; +import reactor.util.annotation.Nullable; public interface ResponderLeaseHandler extends Availability { diff --git a/rsocket-core/src/main/java/io/rsocket/lease/package-info.java b/rsocket-core/src/main/java/io/rsocket/lease/package-info.java index ce1956628..342ab27f7 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/package-info.java @@ -21,5 +21,7 @@ * href="https://github.com/rsocket/rsocket/blob/master/Protocol.md#resuming-operation">Resuming * Operation */ -@javax.annotation.ParametersAreNonnullByDefault +@NonNullApi package io.rsocket.lease; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/package-info.java b/rsocket-core/src/main/java/io/rsocket/metadata/package-info.java index b1bc45ff0..3fb9ae1d6 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/package-info.java @@ -19,4 +19,7 @@ * href="https://github.com/rsocket/rsocket/tree/master/Extensions">protocol extensions related * to the use of metadata. */ +@NonNullApi package io.rsocket.metadata; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/package-info.java b/rsocket-core/src/main/java/io/rsocket/package-info.java index 878a56301..6fe74fb38 100644 --- a/rsocket-core/src/main/java/io/rsocket/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/package-info.java @@ -23,4 +23,7 @@ *

    To connect to or start a server see {@link io.rsocket.core.RSocketConnector RSocketConnector} * and {@link io.rsocket.core.RSocketServer RSocketServer} in {@link io.rsocket.core}. */ +@NonNullApi package io.rsocket; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/package-info.java b/rsocket-core/src/main/java/io/rsocket/plugins/package-info.java index 743e3a8a4..fd9e1f01a 100644 --- a/rsocket-core/src/main/java/io/rsocket/plugins/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/plugins/package-info.java @@ -15,4 +15,7 @@ */ /** Contracts for interception of transports, connections, and requests in in RSocket Java. */ +@NonNullApi package io.rsocket.plugins; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java b/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java index 3882103a0..1d5c23bd6 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/SessionManager.java @@ -19,7 +19,7 @@ import io.netty.buffer.ByteBuf; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; public class SessionManager { private volatile boolean isDisposed; diff --git a/rsocket-core/src/main/java/io/rsocket/resume/package-info.java b/rsocket-core/src/main/java/io/rsocket/resume/package-info.java index aaaa3ee9f..98744386a 100644 --- a/rsocket-core/src/main/java/io/rsocket/resume/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/resume/package-info.java @@ -21,5 +21,7 @@ * href="https://github.com/rsocket/rsocket/blob/master/Protocol.md#resuming-operation">Resuming * Operation */ -@javax.annotation.ParametersAreNonnullByDefault +@NonNullApi package io.rsocket.resume; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/transport/package-info.java b/rsocket-core/src/main/java/io/rsocket/transport/package-info.java index 153676324..00536122a 100644 --- a/rsocket-core/src/main/java/io/rsocket/transport/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/transport/package-info.java @@ -15,5 +15,7 @@ */ /** Client and server transport contracts for pluggable transports. */ -@javax.annotation.ParametersAreNonnullByDefault +@NonNullApi package io.rsocket.transport; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java b/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java index f5d747f7f..4cf33fa86 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/util/ByteBufPayload.java @@ -28,7 +28,7 @@ import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; public final class ByteBufPayload extends AbstractReferenceCounted implements Payload { private static final Recycler RECYCLER = diff --git a/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java b/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java index ec73399f1..58f282110 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java +++ b/rsocket-core/src/main/java/io/rsocket/util/DefaultPayload.java @@ -23,7 +23,7 @@ import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import javax.annotation.Nullable; +import reactor.util.annotation.Nullable; /** * An implementation of {@link Payload}. This implementation is not thread-safe, and hence diff --git a/rsocket-core/src/main/java/io/rsocket/util/package-info.java b/rsocket-core/src/main/java/io/rsocket/util/package-info.java index e034672f1..2fac3327f 100644 --- a/rsocket-core/src/main/java/io/rsocket/util/package-info.java +++ b/rsocket-core/src/main/java/io/rsocket/util/package-info.java @@ -15,5 +15,7 @@ */ /** Shared utility classes and {@link io.rsocket.Payload} implementations. */ -@javax.annotation.ParametersAreNonnullByDefault +@NonNullApi package io.rsocket.util; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java b/rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java index 9d8b8354a..8eb5dee09 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/ConnectionSetupPayloadTest.java @@ -82,7 +82,7 @@ private static ByteBuf encodeSetupFrame(boolean leaseEnabled, Payload setupPaylo leaseEnabled, KEEP_ALIVE_INTERVAL, KEEP_ALIVE_MAX_LIFETIME, - null, + Unpooled.EMPTY_BUFFER, METADATA_TYPE, DATA_TYPE, setupPayload); diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index 6f1ecf98b..00f74152a 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -143,6 +143,6 @@ static Stream>> allInteractions() { rSocket -> rSocket.requestResponse(DefaultPayload.create("test")), rSocket -> rSocket.requestStream(DefaultPayload.create("test")), // rSocket -> rSocket.requestChannel(Mono.just(DefaultPayload.create("test"))), - rSocket -> rSocket.metadataPush(DefaultPayload.create("test"))); + rSocket -> rSocket.metadataPush(DefaultPayload.create("", "test"))); } } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameCodecTest.java index 448b5003b..73c3bde5e 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameCodecTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/LeaseFrameCodecTest.java @@ -32,7 +32,7 @@ void leaseAbsentMetadata() { Assertions.assertFalse(FrameHeaderCodec.hasMetadata(lease)); Assertions.assertEquals(ttl, LeaseFrameCodec.ttl(lease)); Assertions.assertEquals(numRequests, LeaseFrameCodec.numRequests(lease)); - Assertions.assertEquals(0, LeaseFrameCodec.metadata(lease).readableBytes()); + Assertions.assertNull(LeaseFrameCodec.metadata(lease)); lease.release(); } diff --git a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameCodecTest.java b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameCodecTest.java index f7d649972..9607ad327 100644 --- a/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameCodecTest.java +++ b/rsocket-core/src/test/java/io/rsocket/frame/SetupFrameCodecTest.java @@ -22,7 +22,7 @@ void testEncodingNoResume() { assertEquals(FrameType.SETUP, FrameHeaderCodec.frameType(frame)); assertFalse(SetupFrameCodec.resumeEnabled(frame)); - assertNull(SetupFrameCodec.resumeToken(frame)); + assertEquals(0, SetupFrameCodec.resumeToken(frame).readableBytes()); assertEquals("metadata_type", SetupFrameCodec.metadataMimeType(frame)); assertEquals("data_type", SetupFrameCodec.dataMimeType(frame)); assertEquals(metadata, SetupFrameCodec.metadata(frame)); diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/package-info.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/package-info.java new file mode 100644 index 000000000..55ce5646c --- /dev/null +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/filter/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +@NonNullApi +package io.rsocket.client.filter; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/client/package-info.java b/rsocket-load-balancer/src/main/java/io/rsocket/client/package-info.java new file mode 100644 index 000000000..ec21dee96 --- /dev/null +++ b/rsocket-load-balancer/src/main/java/io/rsocket/client/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +@NonNullApi +package io.rsocket.client; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-load-balancer/src/main/java/io/rsocket/stat/package-info.java b/rsocket-load-balancer/src/main/java/io/rsocket/stat/package-info.java new file mode 100644 index 000000000..cfb071175 --- /dev/null +++ b/rsocket-load-balancer/src/main/java/io/rsocket/stat/package-info.java @@ -0,0 +1,20 @@ +/* + * 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. + */ + +@NonNullApi +package io.rsocket.stat; + +import reactor.util.annotation.NonNullApi; diff --git a/rsocket-micrometer/build.gradle b/rsocket-micrometer/build.gradle index 5f2aeb16f..4be616623 100644 --- a/rsocket-micrometer/build.gradle +++ b/rsocket-micrometer/build.gradle @@ -27,8 +27,6 @@ dependencies { implementation 'org.slf4j:slf4j-api' - compileOnly 'com.google.code.findbugs:jsr305' - testImplementation project(':rsocket-test') testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' diff --git a/rsocket-test/build.gradle b/rsocket-test/build.gradle index 3009b5135..5ec1a8061 100644 --- a/rsocket-test/build.gradle +++ b/rsocket-test/build.gradle @@ -26,8 +26,6 @@ dependencies { api 'org.hdrhistogram:HdrHistogram' api 'org.junit.jupiter:junit-jupiter-api' - compileOnly 'com.google.code.findbugs:jsr305' - implementation 'io.projectreactor:reactor-test' implementation 'org.assertj:assertj-core' implementation 'org.mockito:mockito-core' diff --git a/rsocket-transport-local/build.gradle b/rsocket-transport-local/build.gradle index 8c3226065..a5ba84d5c 100644 --- a/rsocket-transport-local/build.gradle +++ b/rsocket-transport-local/build.gradle @@ -24,8 +24,6 @@ plugins { dependencies { api project(':rsocket-core') - compileOnly 'com.google.code.findbugs:jsr305' - testImplementation project(':rsocket-test') testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' diff --git a/rsocket-transport-netty/build.gradle b/rsocket-transport-netty/build.gradle index 0aac12d5c..64e483c90 100644 --- a/rsocket-transport-netty/build.gradle +++ b/rsocket-transport-netty/build.gradle @@ -32,8 +32,6 @@ dependencies { api 'io.projectreactor.netty:reactor-netty' api 'org.slf4j:slf4j-api' - compileOnly 'com.google.code.findbugs:jsr305' - testImplementation project(':rsocket-test') testImplementation 'io.projectreactor:reactor-test' testImplementation 'org.assertj:assertj-core' diff --git a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java index 8be019f1c..22f139310 100644 --- a/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java +++ b/rsocket-transport-netty/src/main/java/io/rsocket/transport/netty/client/TcpClientTransport.java @@ -75,7 +75,7 @@ public static TcpClientTransport create(String bindAddress, int port) { public static TcpClientTransport create(InetSocketAddress address) { Objects.requireNonNull(address, "address must not be null"); - TcpClient tcpClient = TcpClient.create().addressSupplier(() -> address); + TcpClient tcpClient = TcpClient.create().remoteAddress(() -> address); return create(tcpClient); } 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 6991728ca..747401210 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 @@ -35,6 +35,7 @@ import java.util.function.Supplier; import reactor.core.publisher.Mono; import reactor.netty.http.client.HttpClient; +import reactor.netty.http.client.WebsocketClientSpec; import reactor.netty.tcp.TcpClient; /** @@ -47,7 +48,7 @@ public final class WebsocketClientTransport implements ClientTransport, Transpor private final HttpClient client; - private String path; + private final String path; private Supplier> transportHeaders = Collections::emptyMap; @@ -92,7 +93,7 @@ public static WebsocketClientTransport create(String bindAddress, int port) { public static WebsocketClientTransport create(InetSocketAddress address) { Objects.requireNonNull(address, "address must not be null"); - TcpClient client = TcpClient.create().addressSupplier(() -> address); + TcpClient client = TcpClient.create().remoteAddress(() -> address); return create(client); } @@ -155,7 +156,8 @@ public Mono connect(int mtu) { ? isError : client .headers(headers -> transportHeaders.get().forEach(headers::set)) - .websocket(FRAME_LENGTH_MASK) + .websocket( + WebsocketClientSpec.builder().maxFramePayloadLength(FRAME_LENGTH_MASK).build()) .uri(path) .connect() .map( 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 f6e83bc36..c0340c7a2 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 @@ -28,7 +28,7 @@ */ public final class CloseableChannel implements Closeable { - private DisposableChannel channel; + private final DisposableChannel channel; /** * Creates a new instance diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java index b77de6d4e..95bebd6aa 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/TcpSecureTransportTest.java @@ -20,7 +20,7 @@ public class TcpSecureTransportTest implements TransportTest { (address, server) -> TcpClientTransport.create( TcpClient.create() - .addressSupplier(server::address) + .remoteAddress(server::address) .secure( ssl -> ssl.sslContext( @@ -31,7 +31,7 @@ public class TcpSecureTransportTest implements TransportTest { SelfSignedCertificate ssc = new SelfSignedCertificate(); TcpServer server = TcpServer.create() - .addressSupplier(() -> address) + .bindAddress(() -> address) .secure( ssl -> ssl.sslContext( diff --git a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketSecureTransportTest.java b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketSecureTransportTest.java index c1d608979..ec33060b2 100644 --- a/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketSecureTransportTest.java +++ b/rsocket-transport-netty/src/test/java/io/rsocket/transport/netty/WebsocketSecureTransportTest.java @@ -38,7 +38,7 @@ final class WebsocketSecureTransportTest implements TransportTest { (address, server) -> WebsocketClientTransport.create( HttpClient.create() - .addressSupplier(server::address) + .remoteAddress(server::address) .secure( ssl -> ssl.sslContext( @@ -53,7 +53,7 @@ final class WebsocketSecureTransportTest implements TransportTest { HttpServer server = HttpServer.from( TcpServer.create() - .addressSupplier(() -> address) + .bindAddress(() -> address) .secure( ssl -> ssl.sslContext( From fdc9b1bc50cedbb49366c20cf639959e55c0c75a Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 10 May 2020 23:34:59 +0300 Subject: [PATCH 173/181] removes limit rate operator (#829) --- .../io/rsocket/core/RSocketRequester.java | 1 - .../io/rsocket/core/RSocketResponder.java | 6 +- .../plugins/LimitRateInterceptorExample.java | 168 ++++++++++++++++++ .../java/io/rsocket/test/TransportTest.java | 3 +- 4 files changed, 170 insertions(+), 8 deletions(-) create mode 100644 rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/plugins/LimitRateInterceptorExample.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 068797d39..3de074953 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -491,7 +491,6 @@ void hookOnFirstRequest(long n) { receivers.put(streamId, receiver); inboundFlux - .limitRate(Queues.SMALL_BUFFER_SIZE) .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER) .subscribe(upstreamSubscriber); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index dce182b49..d5f9206d8 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -47,7 +47,6 @@ import reactor.core.Exceptions; import reactor.core.publisher.*; import reactor.util.annotation.Nullable; -import reactor.util.concurrent.Queues; /** Responder side of RSocket. Receives {@link ByteBuf}s from a peer's {@link RSocketRequester} */ class RSocketResponder implements RSocket { @@ -526,10 +525,7 @@ protected void hookFinally(SignalType type) { }; sendingSubscriptions.put(streamId, subscriber); - response - .limitRate(Queues.SMALL_BUFFER_SIZE) - .doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER) - .subscribe(subscriber); + response.doOnDiscard(ReferenceCounted.class, DROPPED_ELEMENTS_CONSUMER).subscribe(subscriber); } private void handleChannel(int streamId, Payload payload, long initialRequestN) { 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 new file mode 100644 index 000000000..ac473d7b1 --- /dev/null +++ b/rsocket-examples/src/main/java/io/rsocket/examples/transport/tcp/plugins/LimitRateInterceptorExample.java @@ -0,0 +1,168 @@ +package io.rsocket.examples.transport.tcp.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.examples.transport.tcp.stream.StreamingClient; +import io.rsocket.plugins.RSocketInterceptor; +import io.rsocket.transport.netty.client.TcpClientTransport; +import io.rsocket.transport.netty.server.TcpServerTransport; +import io.rsocket.util.DefaultPayload; +import io.rsocket.util.RSocketProxy; +import java.time.Duration; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import org.reactivestreams.Publisher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import reactor.core.publisher.Flux; +import reactor.util.concurrent.Queues; + +public class LimitRateInterceptorExample { + + private static final Logger logger = LoggerFactory.getLogger(StreamingClient.class); + + public static void main(String[] args) { + BlockingQueue requests = new ArrayBlockingQueue<>(100); + RSocketServer.create( + SocketAcceptor.with( + new RSocket() { + @Override + public Flux requestStream(Payload payload) { + return Flux.interval(Duration.ofMillis(100)) + .doOnRequest(e -> requests.add("Responder requestN(" + e + ")")) + .map(aLong -> DefaultPayload.create("Interval: " + aLong)); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return Flux.from(payloads) + .doOnRequest(e -> requests.add("Responder requestN(" + e + ")")); + } + })) + .interceptors( + ir -> + ir.forRequester(LimitRateInterceptor.forRequester()) + .forResponder(LimitRateInterceptor.forResponder())) + .bind(TcpServerTransport.create("localhost", 7000)) + .subscribe(); + + RSocket socket = + RSocketConnector.create() + .interceptors( + ir -> + ir.forRequester(LimitRateInterceptor.forRequester()) + .forResponder(LimitRateInterceptor.forResponder())) + .connect(TcpClientTransport.create("localhost", 7000)) + .block(); + + socket + .requestStream(DefaultPayload.create("Hello")) + .doOnRequest(e -> requests.add("Requester requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .block(); + + requests.forEach(request -> logger.debug("Requested : {}", request)); + requests.clear(); + + logger.debug("-----------------------------------------------------------------"); + logger.debug("Does requestChannel"); + socket + .requestChannel( + Flux.generate( + () -> 1L, + (s, sink) -> { + sink.next(DefaultPayload.create("Next " + s)); + return ++s; + }) + .doOnRequest(e -> requests.add("Requester Upstream requestN(" + e + ")"))) + .doOnRequest(e -> requests.add("Requester Downstream requestN(" + e + ")")) + .map(Payload::getDataUtf8) + .doOnNext(logger::debug) + .take(10) + .then() + .doFinally(signalType -> socket.dispose()) + .then() + .block(); + + requests.forEach(request -> logger.debug("Requested : {}", request)); + } + + static class LimitRateInterceptor implements RSocketInterceptor { + + final boolean requesterSide; + final int highTide; + final int lowTide; + + LimitRateInterceptor(boolean requesterSide, int highTide, int lowTide) { + this.requesterSide = requesterSide; + this.highTide = highTide; + this.lowTide = lowTide; + } + + @Override + public RSocket apply(RSocket socket) { + return new LimitRateRSocket(socket, requesterSide, highTide, lowTide); + } + + public static LimitRateInterceptor forRequester() { + return forRequester(Queues.SMALL_BUFFER_SIZE); + } + + public static LimitRateInterceptor forRequester(int limit) { + return forRequester(limit, limit); + } + + public static LimitRateInterceptor forRequester(int highTide, int lowTide) { + return new LimitRateInterceptor(true, highTide, lowTide); + } + + public static LimitRateInterceptor forResponder() { + return forRequester(Queues.SMALL_BUFFER_SIZE); + } + + public static LimitRateInterceptor forResponder(int limit) { + return forRequester(limit, limit); + } + + public static LimitRateInterceptor forResponder(int highTide, int lowTide) { + return new LimitRateInterceptor(false, highTide, lowTide); + } + } + + static class LimitRateRSocket extends RSocketProxy { + + final boolean requesterSide; + final int highTide; + final int lowTide; + + public LimitRateRSocket(RSocket source, boolean requesterSide, int highTide, int lowTide) { + super(source); + this.requesterSide = requesterSide; + this.highTide = highTide; + this.lowTide = lowTide; + } + + @Override + public Flux requestStream(Payload payload) { + Flux flux = super.requestStream(payload); + if (requesterSide) { + return flux; + } + return flux.limitRate(highTide, lowTide); + } + + @Override + public Flux requestChannel(Publisher payloads) { + if (requesterSide) { + return super.requestChannel(Flux.from(payloads).limitRate(highTide, lowTide)); + } + return super.requestChannel(payloads).limitRate(highTide, lowTide); + } + } +} diff --git a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java index 583f58634..fc059c7d1 100644 --- a/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java +++ b/rsocket-test/src/main/java/io/rsocket/test/TransportTest.java @@ -237,8 +237,7 @@ default void requestChannel3() { .expectComplete() .verify(getTimeout()); - Assertions.assertThat(requested.get()) - .isEqualTo(256L); // 256 because of eager behavior of limitRate + Assertions.assertThat(requested.get()).isEqualTo(3L); } @DisplayName("makes 1 requestChannel request with 512 payloads") From 69ca8185920d60ef4f20b41db0c19b330bae7330 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Sun, 10 May 2020 23:35:41 +0300 Subject: [PATCH 174/181] provides `bindNow` shortcut for the server (#830) --- .../src/main/java/io/rsocket/core/RSocketServer.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index 66e2249b5..a0f7c810b 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -290,6 +290,14 @@ public Mono get() { }); } + /** + * Start the server on the given transport. Effectively is a shortcut for {@code + * .bind(ServerTransport).block()} + */ + public T bindNow(ServerTransport transport) { + return bind(transport).block(); + } + /** * An alternative to {@link #bind(ServerTransport)} that is useful for installing RSocket on a * server that is started independently. From 31c1509f9a31c006cb9635d78e54dd0924377614 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 11 May 2020 15:31:52 +0300 Subject: [PATCH 175/181] removes deprecated errorConsumer (#832) --- .../main/java/io/rsocket/RSocketFactory.java | 20 ++-- .../io/rsocket/core/RSocketConnector.java | 16 ---- .../io/rsocket/core/RSocketRequester.java | 38 ++++---- .../io/rsocket/core/RSocketResponder.java | 27 +++--- .../java/io/rsocket/core/RSocketServer.java | 20 +--- .../ClientServerInputMultiplexer.java | 5 +- .../rsocket/lease/ResponderLeaseHandler.java | 6 +- .../io/rsocket/core/AbstractSocketRule.java | 10 -- .../java/io/rsocket/core/KeepAliveTest.java | 55 ++--------- .../io/rsocket/core/RSocketLeaseTest.java | 4 +- .../core/RSocketRequesterSubscribersTest.java | 1 - .../io/rsocket/core/RSocketRequesterTest.java | 25 ++--- .../io/rsocket/core/RSocketResponderTest.java | 17 +--- .../java/io/rsocket/core/RSocketTest.java | 93 ++++--------------- .../io/rsocket/core/SetupRejectionTest.java | 6 -- 15 files changed, 90 insertions(+), 253 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java index 178cc4fa9..e23bcceb2 100644 --- a/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java +++ b/rsocket-core/src/main/java/io/rsocket/RSocketFactory.java @@ -111,7 +111,7 @@ public static class ClientRSocketFactory implements ClientTransportAcceptor { private Resume resume; public ClientRSocketFactory() { - this(RSocketConnector.create().errorConsumer(Throwable::printStackTrace)); + this(RSocketConnector.create()); } public ClientRSocketFactory(RSocketConnector connector) { @@ -393,9 +393,13 @@ public ClientRSocketFactory fragment(int mtu) { return this; } - /** @deprecated this is deprecated with no replacement. */ + /** + * @deprecated this handler is deliberately no-ops and is deprecated with no replacement. In + * order to observe errors, it is recommended to add error handler using {@code doOnError} + * on the specific logical stream. In order to observe connection, or RSocket terminal + * errors, it is recommended to hook on {@link Closeable#onClose()} handler. + */ public ClientRSocketFactory errorConsumer(Consumer errorConsumer) { - connector.errorConsumer(errorConsumer); return this; } @@ -417,7 +421,7 @@ public static class ServerRSocketFactory implements ServerTransportAcceptor { private Resume resume; public ServerRSocketFactory() { - this(RSocketServer.create().errorConsumer(Throwable::printStackTrace)); + this(RSocketServer.create()); } public ServerRSocketFactory(RSocketServer server) { @@ -497,9 +501,13 @@ public ServerRSocketFactory fragment(int mtu) { return this; } - /** @deprecated this is deprecated with no replacement. */ + /** + * @deprecated this handler is deliberately no-ops and is deprecated with no replacement. In + * order to observe errors, it is recommended to add error handler using {@code doOnError} + * on the specific logical stream. In order to observe connection, or RSocket terminal + * errors, it is recommended to hook on {@link Closeable#onClose()} handler. + */ public ServerRSocketFactory errorConsumer(Consumer errorConsumer) { - server.errorConsumer(errorConsumer); return this; } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index 38393c27d..d9622e0f0 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -91,8 +91,6 @@ public class RSocketConnector { private int mtu = 0; private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; - private Consumer errorConsumer = ex -> {}; - private RSocketConnector() {} /** @@ -436,17 +434,6 @@ public RSocketConnector payloadDecoder(PayloadDecoder decoder) { return this; } - /** - * @deprecated this is deprecated with no replacement and will be removed after {@link - * io.rsocket.RSocketFactory} is removed. - */ - @Deprecated - public RSocketConnector errorConsumer(Consumer errorConsumer) { - Objects.requireNonNull(errorConsumer); - this.errorConsumer = errorConsumer; - return this; - } - /** * The final step to connect with the transport to use as input and the resulting {@code * Mono} as output. Each subscriber to the returned {@code Mono} starts a new connection @@ -524,7 +511,6 @@ public Mono connect(Supplier transportSupplier) { new RSocketRequester( multiplexer.asClientConnection(), payloadDecoder, - errorConsumer, StreamIdSupplier.clientSupplier(), mtu, (int) keepAliveInterval.toMillis(), @@ -564,7 +550,6 @@ public Mono connect(Supplier transportSupplier) { CLIENT_TAG, wrappedConnection.alloc(), leases.sender(), - errorConsumer, leases.stats()) : ResponderLeaseHandler.None; @@ -573,7 +558,6 @@ public Mono connect(Supplier transportSupplier) { multiplexer.asServerConnection(), wrappedRSocketHandler, payloadDecoder, - errorConsumer, responderLeaseHandler, mtu); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index 3de074953..cdfda0755 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -59,6 +59,8 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.publisher.BaseSubscriber; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -74,9 +76,8 @@ * Requester Side of a RSocket socket. Sends {@link ByteBuf}s to a {@link RSocketResponder} of peer */ class RSocketRequester implements RSocket { - private static final AtomicReferenceFieldUpdater TERMINATION_ERROR = - AtomicReferenceFieldUpdater.newUpdater( - RSocketRequester.class, Throwable.class, "terminationError"); + private static final Logger LOGGER = LoggerFactory.getLogger(RSocketRequester.class); + private static final Exception CLOSED_CHANNEL_EXCEPTION = new ClosedChannelException(); private static final Consumer DROPPED_ELEMENTS_CONSUMER = referenceCounted -> { @@ -93,9 +94,14 @@ class RSocketRequester implements RSocket { CLOSED_CHANNEL_EXCEPTION.setStackTrace(new StackTraceElement[0]); } + private volatile Throwable terminationError; + + private static final AtomicReferenceFieldUpdater TERMINATION_ERROR = + AtomicReferenceFieldUpdater.newUpdater( + RSocketRequester.class, Throwable.class, "terminationError"); + private final DuplexConnection connection; private final PayloadDecoder payloadDecoder; - private final Consumer errorConsumer; private final StreamIdSupplier streamIdSupplier; private final IntObjectMap senders; private final IntObjectMap> receivers; @@ -104,14 +110,12 @@ class RSocketRequester implements RSocket { private final RequesterLeaseHandler leaseHandler; private final ByteBufAllocator allocator; private final KeepAliveFramesAcceptor keepAliveFramesAcceptor; - private volatile Throwable terminationError; private final MonoProcessor onClose; private final Scheduler serialScheduler; RSocketRequester( DuplexConnection connection, PayloadDecoder payloadDecoder, - Consumer errorConsumer, StreamIdSupplier streamIdSupplier, int mtu, int keepAliveTickPeriod, @@ -122,7 +126,6 @@ class RSocketRequester implements RSocket { this.connection = connection; this.allocator = connection.alloc(); this.payloadDecoder = payloadDecoder; - this.errorConsumer = errorConsumer; this.streamIdSupplier = streamIdSupplier; this.mtu = mtu; this.leaseHandler = leaseHandler; @@ -140,7 +143,7 @@ class RSocketRequester implements RSocket { .subscribe(null, this::tryTerminateOnConnectionError, this::tryTerminateOnConnectionClose); connection.send(sendProcessor).subscribe(null, this::handleSendProcessorError); - connection.receive().subscribe(this::handleIncomingFrames, errorConsumer); + connection.receive().subscribe(this::handleIncomingFrames, e -> {}); if (keepAliveTickPeriod != 0 && keepAliveHandler != null) { KeepAliveSupport keepAliveSupport = @@ -396,7 +399,6 @@ private Flux handleChannel(Flux request) { payload.release(); final IllegalArgumentException t = new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); - errorConsumer.accept(t); return Mono.error(t); } return handleChannel(payload, flux); @@ -446,7 +448,6 @@ protected void hookOnNext(Payload payload) { cancel(); final IllegalArgumentException t = new IllegalArgumentException(INVALID_PAYLOAD_ERROR_MESSAGE); - errorConsumer.accept(t); // no need to send any errors. sendProcessor.onNext(CancelFrameCodec.encode(allocator, streamId)); receiver.onError(t); @@ -609,9 +610,9 @@ private void handleStreamZero(FrameType type, ByteBuf frame) { break; default: // Ignore unknown frames. Throwing an error will close the socket. - errorConsumer.accept( - new IllegalStateException( - "Client received supported frame on stream 0: " + frame.toString())); + if (LOGGER.isInfoEnabled()) { + LOGGER.info("Requester received unsupported frame on stream 0: " + frame.toString()); + } } } @@ -668,7 +669,7 @@ private void handleFrame(int streamId, FrameType type, ByteBuf frame) { } default: throw new IllegalStateException( - "Client received supported frame on stream " + streamId + ": " + frame.toString()); + "Requester received unsupported frame on stream " + streamId + ": " + frame.toString()); } } @@ -736,7 +737,9 @@ private void terminate(Throwable e) { try { receiver.onError(e); } catch (Throwable t) { - errorConsumer.accept(t); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Dropped exception", t); + } } }); } @@ -748,14 +751,15 @@ private void terminate(Throwable e) { try { sender.cancel(); } catch (Throwable t) { - errorConsumer.accept(t); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Dropped exception", t); + } } }); } senders.clear(); receivers.clear(); sendProcessor.dispose(); - errorConsumer.accept(e); onClose.onError(e); } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index d5f9206d8..ca5e605c7 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -43,6 +43,8 @@ import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import reactor.core.Disposable; import reactor.core.Exceptions; import reactor.core.publisher.*; @@ -50,6 +52,8 @@ /** Responder side of RSocket. Receives {@link ByteBuf}s from a peer's {@link RSocketRequester} */ class RSocketResponder implements RSocket { + private static final Logger LOGGER = LoggerFactory.getLogger(RSocketResponder.class); + private static final Consumer DROPPED_ELEMENTS_CONSUMER = referenceCounted -> { if (referenceCounted.refCnt() > 0) { @@ -69,7 +73,6 @@ class RSocketResponder implements RSocket { private final io.rsocket.ResponderRSocket responderRSocket; private final PayloadDecoder payloadDecoder; - private final Consumer errorConsumer; private final ResponderLeaseHandler leaseHandler; private final Disposable leaseHandlerDisposable; private final MonoProcessor onClose; @@ -87,12 +90,10 @@ class RSocketResponder implements RSocket { private final UnboundedProcessor sendProcessor; private final ByteBufAllocator allocator; - @SuppressWarnings("deprecation") RSocketResponder( DuplexConnection connection, RSocket requestHandler, PayloadDecoder payloadDecoder, - Consumer errorConsumer, ResponderLeaseHandler leaseHandler, int mtu) { this.connection = connection; @@ -106,7 +107,6 @@ class RSocketResponder implements RSocket { : null; this.payloadDecoder = payloadDecoder; - this.errorConsumer = errorConsumer; this.leaseHandler = leaseHandler; this.sendingSubscriptions = new SynchronizedIntObjectHashMap<>(); this.channelProcessors = new SynchronizedIntObjectHashMap<>(); @@ -118,7 +118,7 @@ class RSocketResponder implements RSocket { connection.send(sendProcessor).subscribe(null, this::handleSendProcessorError); - connection.receive().subscribe(this::handleFrame, errorConsumer); + connection.receive().subscribe(this::handleFrame, e -> {}); leaseHandlerDisposable = leaseHandler.send(sendProcessor::onNextPrioritized); this.connection @@ -135,7 +135,9 @@ private void handleSendProcessorError(Throwable t) { try { subscription.cancel(); } catch (Throwable e) { - errorConsumer.accept(e); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Dropped exception", t); + } } }); @@ -146,7 +148,9 @@ private void handleSendProcessorError(Throwable t) { try { subscription.onError(t); } catch (Throwable e) { - errorConsumer.accept(e); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("Dropped exception", t); + } } }); } @@ -375,9 +379,7 @@ protected void hookOnSubscribe(Subscription subscription) { } @Override - protected void hookOnError(Throwable throwable) { - errorConsumer.accept(throwable); - } + protected void hookOnError(Throwable throwable) {} @Override protected void hookFinally(SignalType type) { @@ -583,9 +585,7 @@ protected void hookOnSubscribe(Subscription subscription) { } @Override - protected void hookOnError(Throwable throwable) { - errorConsumer.accept(throwable); - } + protected void hookOnError(Throwable throwable) {} }); } @@ -599,7 +599,6 @@ private void handleCancelFrame(int streamId) { } private void handleError(int streamId, Throwable t) { - errorConsumer.accept(t); sendProcessor.onNext(ErrorFrameCodec.encode(allocator, streamId, t)); } diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index a0f7c810b..6390d44dd 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -67,8 +67,6 @@ public final class RSocketServer { private int mtu = 0; private PayloadDecoder payloadDecoder = PayloadDecoder.DEFAULT; - private Consumer errorConsumer = ex -> {}; - private RSocketServer() {} /** Static factory method to create an {@code RSocketServer}. */ @@ -247,16 +245,6 @@ public RSocketServer payloadDecoder(PayloadDecoder decoder) { return this; } - /** - * @deprecated this is deprecated with no replacement and will be removed after {@link - * io.rsocket.RSocketFactory} is removed. - */ - @Deprecated - public RSocketServer errorConsumer(Consumer errorConsumer) { - this.errorConsumer = errorConsumer; - return this; - } - /** * Start the server on the given transport. * @@ -395,7 +383,6 @@ private Mono acceptSetup( new RSocketRequester( wrappedMultiplexer.asServerConnection(), payloadDecoder, - errorConsumer, StreamIdSupplier.serverSupplier(), mtu, setupPayload.keepAliveInterval(), @@ -422,11 +409,7 @@ private Mono acceptSetup( ResponderLeaseHandler responderLeaseHandler = leaseEnabled ? new ResponderLeaseHandler.Impl<>( - SERVER_TAG, - connection.alloc(), - leases.sender(), - errorConsumer, - leases.stats()) + SERVER_TAG, connection.alloc(), leases.sender(), leases.stats()) : ResponderLeaseHandler.None; RSocket rSocketResponder = @@ -434,7 +417,6 @@ private Mono acceptSetup( connection, wrappedRSocketHandler, payloadDecoder, - errorConsumer, responderLeaseHandler, mtu); }) diff --git a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java index 0d24c51d8..038120efc 100644 --- a/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java +++ b/rsocket-core/src/main/java/io/rsocket/internal/ClientServerInputMultiplexer.java @@ -119,10 +119,7 @@ public ClientServerInputMultiplexer( break; } }, - t -> { - LOGGER.error("Error receiving frame:", t); - dispose(); - }); + t -> {}); } public DuplexConnection asClientServerConnection() { diff --git a/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java b/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java index 2035ade87..df8787cb7 100644 --- a/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java +++ b/rsocket-core/src/main/java/io/rsocket/lease/ResponderLeaseHandler.java @@ -41,7 +41,6 @@ final class Impl implements ResponderLeaseHandler { private final String tag; private final ByteBufAllocator allocator; private final Function, Flux> leaseSender; - private final Consumer errorConsumer; private final Optional leaseStatsOption; private final T leaseStats; @@ -49,12 +48,10 @@ public Impl( String tag, ByteBufAllocator allocator, Function, Flux> leaseSender, - Consumer errorConsumer, Optional leaseStatsOption) { this.tag = tag; this.allocator = allocator; this.leaseSender = leaseSender; - this.errorConsumer = errorConsumer; this.leaseStatsOption = leaseStatsOption; this.leaseStats = leaseStatsOption.orElse(null); } @@ -86,8 +83,7 @@ public Disposable send(Consumer leaseFrameSender) { lease -> { currentLease = create(lease); leaseFrameSender.accept(createLeaseFrame(lease)); - }, - errorConsumer); + }); } @Override diff --git a/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java index 20972a0d3..ac5832aaa 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java +++ b/rsocket-core/src/test/java/io/rsocket/core/AbstractSocketRule.java @@ -21,8 +21,6 @@ import io.rsocket.buffer.LeaksTrackingByteBufAllocator; import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.test.util.TestSubscriber; -import java.util.concurrent.ConcurrentLinkedQueue; -import org.junit.Assert; import org.junit.rules.ExternalResource; import org.junit.runner.Description; import org.junit.runners.model.Statement; @@ -33,7 +31,6 @@ public abstract class AbstractSocketRule extends ExternalReso protected TestDuplexConnection connection; protected Subscriber connectSub; protected T socket; - protected ConcurrentLinkedQueue errors; protected LeaksTrackingByteBufAllocator allocator; @Override @@ -44,7 +41,6 @@ public void evaluate() throws Throwable { allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); connection = new TestDuplexConnection(allocator); connectSub = TestSubscriber.create(); - errors = new ConcurrentLinkedQueue<>(); init(); base.evaluate(); } @@ -57,12 +53,6 @@ protected void init() { protected abstract T newRSocket(); - public void assertNoConnectionErrors() { - if (errors.size() > 1) { - Assert.fail("No connection errors expected: " + errors.peek().toString()); - } - } - public ByteBufAllocator alloc() { return allocator; } diff --git a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java index b3ded08ec..d98f86113 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/KeepAliveTest.java @@ -35,9 +35,6 @@ import io.rsocket.test.util.TestDuplexConnection; import io.rsocket.util.DefaultPayload; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.function.Consumer; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -57,12 +54,10 @@ static RSocketState requester(int tickPeriod, int timeout) { LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); TestDuplexConnection connection = new TestDuplexConnection(allocator); - Errors errors = new Errors(); RSocketRequester rSocket = new RSocketRequester( connection, DefaultPayload::create, - errors, StreamIdSupplier.clientSupplier(), 0, tickPeriod, @@ -70,7 +65,7 @@ static RSocketState requester(int tickPeriod, int timeout) { new DefaultKeepAliveHandler(connection), RequesterLeaseHandler.None, TestScheduler.INSTANCE); - return new RSocketState(rSocket, errors, allocator, connection); + return new RSocketState(rSocket, allocator, connection); } static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { @@ -85,12 +80,10 @@ static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { Duration.ofSeconds(10), false); - Errors errors = new Errors(); RSocketRequester rSocket = new RSocketRequester( resumableConnection, DefaultPayload::create, - errors, StreamIdSupplier.clientSupplier(), 0, tickPeriod, @@ -98,7 +91,7 @@ static ResumableRSocketState resumableRequester(int tickPeriod, int timeout) { new ResumableKeepAliveHandler(resumableConnection), RequesterLeaseHandler.None, TestScheduler.INSTANCE); - return new ResumableRSocketState(rSocket, errors, connection, resumableConnection, allocator); + return new ResumableRSocketState(rSocket, connection, resumableConnection, allocator); } @BeforeEach @@ -121,10 +114,8 @@ void rSocketNotDisposedOnPresentKeepAlives() { Mono.delay(Duration.ofMillis(2000)).block(); RSocket rSocket = requesterState.rSocket(); - List errors = requesterState.errors().errors(); Assertions.assertThat(rSocket.isDisposed()).isFalse(); - Assertions.assertThat(errors).isEmpty(); } @Test @@ -143,11 +134,12 @@ void rSocketDisposedOnMissingKeepAlives() { Mono.delay(Duration.ofMillis(2000)).block(); - List errors = requesterState.errors().errors(); Assertions.assertThat(rSocket.isDisposed()).isTrue(); - Assertions.assertThat(errors).hasSize(1); - Throwable throwable = errors.get(0); - Assertions.assertThat(throwable).isInstanceOf(ConnectionErrorException.class); + rSocket + .onClose() + .as(StepVerifier::create) + .expectError(ConnectionErrorException.class) + .verify(Duration.ofMillis(100)); } @Test @@ -224,13 +216,11 @@ void resumableRequesterNoKeepAlivesAfterDispose() { @Test void resumableRSocketsNotDisposedOnMissingKeepAlives() { RSocket rSocket = resumableRequesterState.rSocket(); - List errors = resumableRequesterState.errors().errors(); TestDuplexConnection connection = resumableRequesterState.connection(); Mono.delay(Duration.ofMillis(500)).block(); Assertions.assertThat(rSocket.isDisposed()).isFalse(); - Assertions.assertThat(errors).hasSize(0); Assertions.assertThat(connection.isDisposed()).isTrue(); } @@ -248,17 +238,12 @@ private boolean keepAliveFrameWithoutRespondFlag(ByteBuf frame) { static class RSocketState { private final RSocket rSocket; - private final Errors errors; private final TestDuplexConnection connection; private final LeaksTrackingByteBufAllocator allocator; public RSocketState( - RSocket rSocket, - Errors errors, - LeaksTrackingByteBufAllocator allocator, - TestDuplexConnection connection) { + RSocket rSocket, LeaksTrackingByteBufAllocator allocator, TestDuplexConnection connection) { this.rSocket = rSocket; - this.errors = errors; this.connection = connection; this.allocator = allocator; } @@ -271,10 +256,6 @@ public RSocket rSocket() { return rSocket; } - public Errors errors() { - return errors; - } - public LeaksTrackingByteBufAllocator alloc() { return allocator; } @@ -282,19 +263,16 @@ public LeaksTrackingByteBufAllocator alloc() { static class ResumableRSocketState { private final RSocket rSocket; - private final Errors errors; private final TestDuplexConnection connection; private final ResumableDuplexConnection resumableDuplexConnection; private final LeaksTrackingByteBufAllocator allocator; public ResumableRSocketState( RSocket rSocket, - Errors errors, TestDuplexConnection connection, ResumableDuplexConnection resumableDuplexConnection, LeaksTrackingByteBufAllocator allocator) { this.rSocket = rSocket; - this.errors = errors; this.connection = connection; this.resumableDuplexConnection = resumableDuplexConnection; this.allocator = allocator; @@ -312,25 +290,8 @@ public RSocket rSocket() { return rSocket; } - public Errors errors() { - return errors; - } - public LeaksTrackingByteBufAllocator alloc() { return allocator; } } - - static class Errors implements Consumer { - private final List errors = new ArrayList<>(); - - @Override - public void accept(Throwable throwable) { - errors.add(throwable); - } - - public List errors() { - return new ArrayList<>(errors); - } - } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java index ddfbe4234..ab336b8cd 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketLeaseTest.java @@ -85,7 +85,7 @@ void setUp() { requesterLeaseHandler = new RequesterLeaseHandler.Impl(TAG, leases -> leaseReceiver = leases); responderLeaseHandler = new ResponderLeaseHandler.Impl<>( - TAG, byteBufAllocator, stats -> leaseSender, err -> {}, Optional.empty()); + TAG, byteBufAllocator, stats -> leaseSender, Optional.empty()); ClientServerInputMultiplexer multiplexer = new ClientServerInputMultiplexer(connection, new InitializingInterceptorRegistry(), true); @@ -93,7 +93,6 @@ void setUp() { new RSocketRequester( multiplexer.asClientConnection(), payloadDecoder, - err -> {}, StreamIdSupplier.clientSupplier(), 0, 0, @@ -114,7 +113,6 @@ void setUp() { multiplexer.asServerConnection(), mockRSocketHandler, payloadDecoder, - err -> {}, responderLeaseHandler, 0); } diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java index 00f74152a..4cd3a3a26 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterSubscribersTest.java @@ -66,7 +66,6 @@ void setUp() { new RSocketRequester( connection, PayloadDecoder.DEFAULT, - err -> {}, StreamIdSupplier.clientSupplier(), 0, 0, diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java index e4943e9b0..1ba75f75a 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketRequesterTest.java @@ -24,10 +24,8 @@ import static io.rsocket.frame.FrameType.REQUEST_RESPONSE; import static io.rsocket.frame.FrameType.REQUEST_STREAM; import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.verify; @@ -122,13 +120,9 @@ public void tearDown() { @Test @Timeout(2_000) - public void testInvalidFrameOnStream0() { + public void testInvalidFrameOnStream0ShouldNotTerminateRSocket() { rule.connection.addToReceivedBuffer(RequestNFrameCodec.encode(rule.alloc(), 0, 10)); - assertThat("Unexpected errors.", rule.errors, hasSize(1)); - assertThat( - "Unexpected error received.", - rule.errors, - contains(instanceOf(IllegalStateException.class))); + Assertions.assertThat(rule.socket.isDisposed()).isFalse(); rule.assertHasNoLeaks(); } @@ -167,11 +161,8 @@ protected void hookOnSubscribe(Subscription subscription) { public void testHandleSetupException() { rule.connection.addToReceivedBuffer( ErrorFrameCodec.encode(rule.alloc(), 0, new RejectedSetupException("boom"))); - assertThat("Unexpected errors.", rule.errors, hasSize(1)); - assertThat( - "Unexpected error received.", - rule.errors, - contains(instanceOf(RejectedSetupException.class))); + Assertions.assertThatThrownBy(() -> rule.socket.onClose().block()) + .isInstanceOf(RejectedSetupException.class); rule.assertHasNoLeaks(); } @@ -242,7 +233,12 @@ public void testRequestReplyErrorOnSend() { Subscriber responseSub = TestSubscriber.create(10); response.subscribe(responseSub); - this.rule.assertNoConnectionErrors(); + this.rule + .socket + .onClose() + .as(StepVerifier::create) + .expectComplete() + .verify(Duration.ofMillis(100)); verify(responseSub).onSubscribe(any(Subscription.class)); @@ -1006,7 +1002,6 @@ protected RSocketRequester newRSocket() { return new RSocketRequester( connection, PayloadDecoder.ZERO_COPY, - throwable -> errors.add(throwable), StreamIdSupplier.clientSupplier(), 0, 0, diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java index e34973848..036dc2eef 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketResponderTest.java @@ -59,7 +59,6 @@ import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; import java.util.Collection; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ThreadLocalRandom; import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Stream; @@ -138,7 +137,6 @@ public void testHandleResponseFrameNoError() throws Exception { Collection> sendSubscribers = rule.connection.getSendSubscribers(); assertThat("Request not sent.", sendSubscribers, hasSize(1)); - assertThat("Unexpected error.", rule.errors, is(empty())); Subscriber sendSub = sendSubscribers.iterator().next(); assertThat( "Unexpected frame sent.", @@ -152,7 +150,6 @@ public void testHandleResponseFrameNoError() throws Exception { public void testHandlerEmitsError() throws Exception { final int streamId = 4; rule.sendRequest(streamId, FrameType.REQUEST_STREAM); - assertThat("Unexpected error.", rule.errors, is(empty())); assertThat( "Unexpected frame sent.", frameType(rule.connection.awaitSend()), is(FrameType.ERROR)); } @@ -173,7 +170,6 @@ public Mono requestResponse(Payload payload) { }); rule.sendRequest(streamId, FrameType.REQUEST_RESPONSE); - assertThat("Unexpected error.", rule.errors, is(empty())); assertThat("Unexpected frame sent.", rule.connection.getSent(), is(empty())); rule.connection.addToReceivedBuffer(CancelFrameCodec.encode(allocator, streamId)); @@ -232,10 +228,6 @@ protected void hookOnSubscribe(Subscription subscription) { for (Runnable runnable : runnables) { rule.connection.clearSendReceiveBuffers(); runnable.run(); - Assertions.assertThat(rule.errors) - .first() - .isInstanceOf(IllegalArgumentException.class) - .hasToString("java.lang.IllegalArgumentException: " + INVALID_PAYLOAD_ERROR_MESSAGE); Assertions.assertThat(rule.connection.getSent()) .hasSize(1) .first() @@ -778,7 +770,6 @@ public void setAcceptingSocket(RSocket acceptingSocket) { this.acceptingSocket = acceptingSocket; connection = new TestDuplexConnection(alloc()); connectSub = TestSubscriber.create(); - errors = new ConcurrentLinkedQueue<>(); this.prefetch = Integer.MAX_VALUE; super.init(); } @@ -787,7 +778,6 @@ public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { this.acceptingSocket = acceptingSocket; connection = new TestDuplexConnection(alloc()); connectSub = TestSubscriber.create(); - errors = new ConcurrentLinkedQueue<>(); this.prefetch = prefetch; super.init(); } @@ -795,12 +785,7 @@ public void setAcceptingSocket(RSocket acceptingSocket, int prefetch) { @Override protected RSocketResponder newRSocket() { return new RSocketResponder( - connection, - acceptingSocket, - PayloadDecoder.ZERO_COPY, - throwable -> errors.add(throwable), - ResponderLeaseHandler.None, - 0); + connection, acceptingSocket, PayloadDecoder.ZERO_COPY, ResponderLeaseHandler.None, 0); } private void sendRequest(int streamId, FrameType frameType) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index 48ce150d6..f032942db 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -16,11 +16,6 @@ package io.rsocket.core; -import static org.hamcrest.Matchers.empty; -import static org.hamcrest.Matchers.is; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; - import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufAllocator; import io.rsocket.Payload; @@ -34,24 +29,18 @@ import io.rsocket.lease.RequesterLeaseHandler; import io.rsocket.lease.ResponderLeaseHandler; import io.rsocket.test.util.LocalDuplexConnection; -import io.rsocket.test.util.TestSubscriber; import io.rsocket.util.DefaultPayload; import io.rsocket.util.EmptyPayload; import java.time.Duration; -import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicReference; import org.assertj.core.api.Assertions; -import org.hamcrest.MatcherAssert; -import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExternalResource; import org.junit.runner.Description; import org.junit.runners.model.Statement; -import org.mockito.ArgumentCaptor; import org.reactivestreams.Publisher; -import org.reactivestreams.Subscriber; import reactor.core.publisher.DirectProcessor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -62,16 +51,6 @@ public class RSocketTest { @Rule public final SocketRule rule = new SocketRule(); - public static void assertError(String s, String mode, ArrayList errors) { - for (Throwable t : errors) { - if (t.toString().equals(s)) { - return; - } - } - - Assert.fail("Expected " + mode + " connection error: " + s + " other errors " + errors.size()); - } - @Test(timeout = 2_000) public void testRequestReplyNoError() { StepVerifier.create(rule.crs.requestResponse(DefaultPayload.create("hello"))) @@ -89,14 +68,15 @@ public Mono requestResponse(Payload payload) { return Mono.error(new NullPointerException("Deliberate exception.")); } }); - Subscriber subscriber = TestSubscriber.create(); - rule.crs.requestResponse(EmptyPayload.INSTANCE).subscribe(subscriber); - verify(subscriber).onError(any(ApplicationErrorException.class)); - - // Client sees error through normal API - rule.assertNoClientErrors(); - - rule.assertServerError("java.lang.NullPointerException: Deliberate exception."); + rule.crs + .requestResponse(EmptyPayload.INSTANCE) + .as(StepVerifier::create) + .expectErrorSatisfies( + t -> + Assertions.assertThat(t) + .isInstanceOf(ApplicationErrorException.class) + .hasMessage("Deliberate exception.")) + .verify(Duration.ofMillis(100)); } @Test(timeout = 2000) @@ -109,21 +89,16 @@ public Mono requestResponse(Payload payload) { new CustomRSocketException(0x00000501, "Deliberate Custom exception.")); } }); - Subscriber subscriber = TestSubscriber.create(); - rule.crs.requestResponse(EmptyPayload.INSTANCE).subscribe(subscriber); - ArgumentCaptor customRSocketExceptionArgumentCaptor = - ArgumentCaptor.forClass(CustomRSocketException.class); - verify(subscriber).onError(customRSocketExceptionArgumentCaptor.capture()); - - Assert.assertEquals( - "Deliberate Custom exception.", - customRSocketExceptionArgumentCaptor.getValue().getMessage()); - Assert.assertEquals(0x00000501, customRSocketExceptionArgumentCaptor.getValue().errorCode()); - - // Client sees error through normal API - rule.assertNoClientErrors(); - - rule.assertServerError("CustomRSocketException (0x501): Deliberate Custom exception."); + rule.crs + .requestResponse(EmptyPayload.INSTANCE) + .as(StepVerifier::create) + .expectErrorSatisfies( + t -> + Assertions.assertThat(t) + .isInstanceOf(CustomRSocketException.class) + .hasMessage("Deliberate Custom exception.") + .hasFieldOrPropertyWithValue("errorCode", 0x00000501)) + .verify(); } @Test(timeout = 2000) @@ -147,9 +122,6 @@ public Flux requestChannel(Publisher payloads) { .expectNextCount(3) .expectComplete() .verify(Duration.ofMillis(5000)); - - rule.assertNoClientErrors(); - rule.assertNoServerErrors(); } @Test(timeout = 2000) @@ -413,8 +385,6 @@ public static class SocketRule extends ExternalResource { private RSocketResponder srs; private RSocket requestAcceptor; - private ArrayList clientErrors = new ArrayList<>(); - private ArrayList serverErrors = new ArrayList<>(); private LeaksTrackingByteBufAllocator allocator; @@ -479,7 +449,6 @@ public Flux requestChannel(Publisher payloads) { serverConnection, requestAcceptor, PayloadDecoder.DEFAULT, - throwable -> serverErrors.add(throwable), ResponderLeaseHandler.None, 0); @@ -487,7 +456,6 @@ public Flux requestChannel(Publisher payloads) { new RSocketRequester( clientConnection, PayloadDecoder.DEFAULT, - throwable -> clientErrors.add(throwable), StreamIdSupplier.clientSupplier(), 0, 0, @@ -501,28 +469,5 @@ public void setRequestAcceptor(RSocket requestAcceptor) { this.requestAcceptor = requestAcceptor; init(); } - - public void assertNoErrors() { - assertNoClientErrors(); - assertNoServerErrors(); - } - - public void assertNoClientErrors() { - MatcherAssert.assertThat( - "Unexpected error on the client connection.", clientErrors, is(empty())); - } - - public void assertNoServerErrors() { - MatcherAssert.assertThat( - "Unexpected error on the server connection.", serverErrors, is(empty())); - } - - public void assertClientError(String s) { - assertError(s, "client", this.clientErrors); - } - - public void assertServerError(String s) { - assertError(s, "server", this.serverErrors); - } } } diff --git a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java index 75a5e070e..2957a051e 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/SetupRejectionTest.java @@ -18,8 +18,6 @@ import io.rsocket.transport.ServerTransport; import io.rsocket.util.DefaultPayload; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -53,12 +51,10 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { LeaksTrackingByteBufAllocator allocator = LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); TestDuplexConnection conn = new TestDuplexConnection(allocator); - List errors = new ArrayList<>(); RSocketRequester rSocket = new RSocketRequester( conn, DefaultPayload::create, - errors::add, StreamIdSupplier.clientSupplier(), 0, 0, @@ -83,7 +79,6 @@ void requesterStreamsTerminatedOnZeroErrorFrame() { err -> err instanceof RejectedSetupException && errorMsg.equals(err.getMessage())) .verify(Duration.ofSeconds(5)); - assertThat(errors).hasSize(1); assertThat(rSocket.isDisposed()).isTrue(); } @@ -96,7 +91,6 @@ void requesterNewStreamsTerminatedAfterZeroErrorFrame() { new RSocketRequester( conn, DefaultPayload::create, - err -> {}, StreamIdSupplier.clientSupplier(), 0, 0, From d880f51862b71e8790a9883a05963991f779e4a8 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Mon, 11 May 2020 16:30:39 +0300 Subject: [PATCH 176/181] fixes onClose behaviour to not error on shutdown (#833) --- .../io/rsocket/core/RSocketRequester.java | 26 ++++++++------- .../io/rsocket/core/RSocketResponder.java | 7 ++-- .../java/io/rsocket/core/RSocketTest.java | 33 +++++++++++++++++++ 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java index cdfda0755..f7fb161fd 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketRequester.java @@ -50,7 +50,6 @@ import io.rsocket.keepalive.KeepAliveSupport; import io.rsocket.lease.RequesterLeaseHandler; import java.nio.channels.ClosedChannelException; -import java.util.concurrent.CancellationException; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; import java.util.function.Consumer; @@ -137,10 +136,7 @@ class RSocketRequester implements RSocket { // DO NOT Change the order here. The Send processor must be subscribed to before receiving this.sendProcessor = new UnboundedProcessor<>(); - connection - .onClose() - .or(onClose) - .subscribe(null, this::tryTerminateOnConnectionError, this::tryTerminateOnConnectionClose); + connection.onClose().subscribe(null, this::tryTerminateOnConnectionError, this::tryShutdown); connection.send(sendProcessor).subscribe(null, this::handleSendProcessorError); connection.receive().subscribe(this::handleIncomingFrames, e -> {}); @@ -188,7 +184,7 @@ public double availability() { @Override public void dispose() { - tryTerminate(() -> new CancellationException("Disposed")); + tryShutdown(); } @Override @@ -708,10 +704,6 @@ private void tryTerminateOnConnectionError(Throwable e) { tryTerminate(() -> e); } - private void tryTerminateOnConnectionClose() { - tryTerminate(() -> CLOSED_CHANNEL_EXCEPTION); - } - private void tryTerminateOnZeroError(ByteBuf errorFrame) { tryTerminate(() -> Exceptions.from(0, errorFrame)); } @@ -725,6 +717,14 @@ private void tryTerminate(Supplier errorSupplier) { } } + private void tryShutdown() { + if (terminationError == null) { + if (TERMINATION_ERROR.compareAndSet(this, null, CLOSED_CHANNEL_EXCEPTION)) { + terminate(CLOSED_CHANNEL_EXCEPTION); + } + } + } + private void terminate(Throwable e) { connection.dispose(); leaseHandler.dispose(); @@ -760,7 +760,11 @@ private void terminate(Throwable e) { senders.clear(); receivers.clear(); sendProcessor.dispose(); - onClose.onError(e); + if (e == CLOSED_CHANNEL_EXCEPTION) { + onClose.onComplete(); + } else { + onClose.onError(e); + } } private void handleSendProcessorError(Throwable t) { diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java index ca5e605c7..d3860e5f2 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketResponder.java @@ -75,7 +75,6 @@ class RSocketResponder implements RSocket { private final PayloadDecoder payloadDecoder; private final ResponderLeaseHandler leaseHandler; private final Disposable leaseHandlerDisposable; - private final MonoProcessor onClose; private volatile Throwable terminationError; private static final AtomicReferenceFieldUpdater TERMINATION_ERROR = @@ -110,7 +109,6 @@ class RSocketResponder implements RSocket { this.leaseHandler = leaseHandler; this.sendingSubscriptions = new SynchronizedIntObjectHashMap<>(); this.channelProcessors = new SynchronizedIntObjectHashMap<>(); - this.onClose = MonoProcessor.create(); // DO NOT Change the order here. The Send processor must be subscribed to before receiving // connections @@ -123,7 +121,6 @@ class RSocketResponder implements RSocket { this.connection .onClose() - .or(onClose) .subscribe(null, this::tryTerminateOnConnectionError, this::tryTerminateOnConnectionClose); } @@ -256,12 +253,12 @@ public void dispose() { @Override public boolean isDisposed() { - return onClose.isDisposed(); + return connection.isDisposed(); } @Override public Mono onClose() { - return onClose; + return connection.onClose(); } private void cleanup(Throwable e) { diff --git a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java index f032942db..692894fd6 100644 --- a/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java +++ b/rsocket-core/src/test/java/io/rsocket/core/RSocketTest.java @@ -41,6 +41,8 @@ import org.junit.runner.Description; import org.junit.runners.model.Statement; import org.reactivestreams.Publisher; +import reactor.core.Disposable; +import reactor.core.Disposables; import reactor.core.publisher.DirectProcessor; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -51,6 +53,34 @@ public class RSocketTest { @Rule public final SocketRule rule = new SocketRule(); + @Test + public void rsocketDisposalShouldEndupWithNoErrorsOnClose() { + RSocket requestHandlingRSocket = + new RSocket() { + final Disposable disposable = Disposables.single(); + + @Override + public void dispose() { + disposable.dispose(); + } + + @Override + public boolean isDisposed() { + return disposable.isDisposed(); + } + }; + rule.setRequestAcceptor(requestHandlingRSocket); + rule.crs + .onClose() + .as(StepVerifier::create) + .expectSubscription() + .then(rule.crs::dispose) + .expectComplete() + .verify(Duration.ofMillis(100)); + + Assertions.assertThat(requestHandlingRSocket.isDisposed()).isTrue(); + } + @Test(timeout = 2_000) public void testRequestReplyNoError() { StepVerifier.create(rule.crs.requestResponse(DefaultPayload.create("hello"))) @@ -413,6 +443,9 @@ protected void init() { LocalDuplexConnection clientConnection = new LocalDuplexConnection("client", allocator, serverProcessor, clientProcessor); + clientConnection.onClose().doFinally(__ -> serverConnection.dispose()).subscribe(); + serverConnection.onClose().doFinally(__ -> clientConnection.dispose()).subscribe(); + requestAcceptor = null != requestAcceptor ? requestAcceptor From a0e741c5af29a1ca16c8acaf316d6ae2067be776 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 11 May 2020 18:18:57 +0100 Subject: [PATCH 177/181] includes LimitRateInterceptor as a built-in interceptor (#834) --- .../io/rsocket/core/RSocketConnector.java | 1 + .../java/io/rsocket/core/RSocketServer.java | 1 + .../rsocket/plugins/LimitRateInterceptor.java | 133 ++++++++++++++++++ .../plugins/LimitRateInterceptorExample.java | 116 +++------------ 4 files changed, 151 insertions(+), 100 deletions(-) create mode 100644 rsocket-core/src/main/java/io/rsocket/plugins/LimitRateInterceptor.java diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java index d9622e0f0..02f1a51cc 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketConnector.java @@ -221,6 +221,7 @@ public RSocketConnector keepAlive(Duration interval, Duration maxLifeTime) { * * @param configurer a configurer to customize interception with. * @return the same instance for method chaining + * @see io.rsocket.plugins.LimitRateInterceptor */ public RSocketConnector interceptors(Consumer configurer) { configurer.accept(this.interceptors); diff --git a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java index 6390d44dd..c5734cecc 100644 --- a/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java +++ b/rsocket-core/src/main/java/io/rsocket/core/RSocketServer.java @@ -142,6 +142,7 @@ public RSocketServer acceptor(SocketAcceptor acceptor) { * * @param configurer a configurer to customize interception with. * @return the same instance for method chaining + * @see io.rsocket.plugins.LimitRateInterceptor */ public RSocketServer interceptors(Consumer configurer) { configurer.accept(this.interceptors); diff --git a/rsocket-core/src/main/java/io/rsocket/plugins/LimitRateInterceptor.java b/rsocket-core/src/main/java/io/rsocket/plugins/LimitRateInterceptor.java new file mode 100644 index 000000000..d7d9742d0 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/plugins/LimitRateInterceptor.java @@ -0,0 +1,133 @@ +/* + * 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.plugins; + +import io.rsocket.Payload; +import io.rsocket.RSocket; +import io.rsocket.util.RSocketProxy; +import org.reactivestreams.Publisher; +import reactor.core.publisher.Flux; + +/** + * Interceptor that adds {@link Flux#limitRate(int, int)} to publishers of outbound streams that + * breaks down or aggregates demand values from the remote end (i.e. {@code REQUEST_N} frames) into + * batches of a uniform size. For example the remote may request {@code Long.MAXVALUE} or it may + * start requesting one at a time, in both cases with the limit set to 64, the publisher will see a + * demand of 64 to start and subsequent batches of 48, i.e. continuing to prefetch and refill an + * internal queue when it falls to 75% full. The high and low tide marks are configurable. + * + *

    See static factory methods to create an instance for a requester or for a responder. + * + *

    Note: keep in mind that the {@code limitRate} operator always uses requests + * the same request values, even if the remote requests less than the limit. For example given a + * limit of 64, if the remote requests 4, 64 will be prefetched of which 4 will be sent and 60 will + * be cached. + * + * @since 1.0 + */ +public class LimitRateInterceptor implements RSocketInterceptor { + + private final int highTide; + private final int lowTide; + private final boolean requesterProxy; + + private LimitRateInterceptor(int highTide, int lowTide, boolean requesterProxy) { + this.highTide = highTide; + this.lowTide = lowTide; + this.requesterProxy = requesterProxy; + } + + @Override + public RSocket apply(RSocket socket) { + return requesterProxy ? new RequesterProxy(socket) : new ResponderProxy(socket); + } + + /** + * Create an interceptor for an {@code RSocket} that handles request-stream and/or request-channel + * interactions. + * + * @param prefetchRate the prefetch rate to pass to {@link Flux#limitRate(int)} + * @return the created interceptor + */ + public static LimitRateInterceptor forResponder(int prefetchRate) { + return forResponder(prefetchRate, prefetchRate); + } + + /** + * Create an interceptor for an {@code RSocket} that handles request-stream and/or request-channel + * interactions with more control over the overall prefetch rate and replenish threshold. + * + * @param highTide the high tide value to pass to {@link Flux#limitRate(int, int)} + * @param lowTide the low tide value to pass to {@link Flux#limitRate(int, int)} + * @return the created interceptor + */ + public static LimitRateInterceptor forResponder(int highTide, int lowTide) { + return new LimitRateInterceptor(highTide, lowTide, false); + } + + /** + * Create an interceptor for an {@code RSocket} that performs request-channel interactions. + * + * @param prefetchRate the prefetch rate to pass to {@link Flux#limitRate(int)} + * @return the created interceptor + */ + public static LimitRateInterceptor forRequester(int prefetchRate) { + return forRequester(prefetchRate, prefetchRate); + } + + /** + * Create an interceptor for an {@code RSocket} that performs request-channel interactions with + * more control over the overall prefetch rate and replenish threshold. + * + * @param highTide the high tide value to pass to {@link Flux#limitRate(int, int)} + * @param lowTide the low tide value to pass to {@link Flux#limitRate(int, int)} + * @return the created interceptor + */ + public static LimitRateInterceptor forRequester(int highTide, int lowTide) { + return new LimitRateInterceptor(highTide, lowTide, true); + } + + /** Responder side proxy, limits response streams. */ + private class ResponderProxy extends RSocketProxy { + + ResponderProxy(RSocket source) { + super(source); + } + + @Override + public Flux requestStream(Payload payload) { + return super.requestStream(payload).limitRate(highTide, lowTide); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return super.requestChannel(payloads).limitRate(highTide, lowTide); + } + } + + /** Requester side proxy, limits channel request stream. */ + private class RequesterProxy extends RSocketProxy { + + RequesterProxy(RSocket source) { + super(source); + } + + @Override + public Flux requestChannel(Publisher payloads) { + return super.requestChannel(Flux.from(payloads).limitRate(highTide, lowTide)); + } + } +} 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 ac473d7b1..67a85b67f 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,73 +5,64 @@ import io.rsocket.SocketAcceptor; import io.rsocket.core.RSocketConnector; import io.rsocket.core.RSocketServer; -import io.rsocket.examples.transport.tcp.stream.StreamingClient; -import io.rsocket.plugins.RSocketInterceptor; +import io.rsocket.plugins.LimitRateInterceptor; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.server.TcpServerTransport; import io.rsocket.util.DefaultPayload; -import io.rsocket.util.RSocketProxy; import java.time.Duration; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; import org.reactivestreams.Publisher; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import reactor.core.publisher.Flux; -import reactor.util.concurrent.Queues; public class LimitRateInterceptorExample { - private static final Logger logger = LoggerFactory.getLogger(StreamingClient.class); + private static final Logger logger = LoggerFactory.getLogger(LimitRateInterceptorExample.class); public static void main(String[] args) { - BlockingQueue requests = new ArrayBlockingQueue<>(100); RSocketServer.create( SocketAcceptor.with( new RSocket() { @Override public Flux requestStream(Payload payload) { return Flux.interval(Duration.ofMillis(100)) - .doOnRequest(e -> requests.add("Responder requestN(" + e + ")")) + .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 -> requests.add("Responder requestN(" + e + ")")); + .doOnRequest( + e -> logger.debug("Server publisher receives request for " + e)); } })) - .interceptors( - ir -> - ir.forRequester(LimitRateInterceptor.forRequester()) - .forResponder(LimitRateInterceptor.forResponder())) + .interceptors(registry -> registry.forResponder(LimitRateInterceptor.forResponder(64))) .bind(TcpServerTransport.create("localhost", 7000)) .subscribe(); RSocket socket = RSocketConnector.create() - .interceptors( - ir -> - ir.forRequester(LimitRateInterceptor.forRequester()) - .forResponder(LimitRateInterceptor.forResponder())) + .interceptors(registry -> registry.forRequester(LimitRateInterceptor.forRequester(64))) .connect(TcpClientTransport.create("localhost", 7000)) .block(); + logger.debug( + "\n\nStart of requestStream interaction\n" + "----------------------------------\n"); + socket .requestStream(DefaultPayload.create("Hello")) - .doOnRequest(e -> requests.add("Requester requestN(" + e + ")")) + .doOnRequest(e -> logger.debug("Client sends requestN(" + e + ")")) .map(Payload::getDataUtf8) .doOnNext(logger::debug) .take(10) .then() .block(); - requests.forEach(request -> logger.debug("Requested : {}", request)); - requests.clear(); + logger.debug( + "\n\nStart of requestChannel interaction\n" + "-----------------------------------\n"); - logger.debug("-----------------------------------------------------------------"); - logger.debug("Does requestChannel"); socket .requestChannel( Flux.generate( @@ -80,8 +71,8 @@ public Flux requestChannel(Publisher payloads) { sink.next(DefaultPayload.create("Next " + s)); return ++s; }) - .doOnRequest(e -> requests.add("Requester Upstream requestN(" + e + ")"))) - .doOnRequest(e -> requests.add("Requester Downstream requestN(" + e + ")")) + .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) @@ -89,80 +80,5 @@ public Flux requestChannel(Publisher payloads) { .doFinally(signalType -> socket.dispose()) .then() .block(); - - requests.forEach(request -> logger.debug("Requested : {}", request)); - } - - static class LimitRateInterceptor implements RSocketInterceptor { - - final boolean requesterSide; - final int highTide; - final int lowTide; - - LimitRateInterceptor(boolean requesterSide, int highTide, int lowTide) { - this.requesterSide = requesterSide; - this.highTide = highTide; - this.lowTide = lowTide; - } - - @Override - public RSocket apply(RSocket socket) { - return new LimitRateRSocket(socket, requesterSide, highTide, lowTide); - } - - public static LimitRateInterceptor forRequester() { - return forRequester(Queues.SMALL_BUFFER_SIZE); - } - - public static LimitRateInterceptor forRequester(int limit) { - return forRequester(limit, limit); - } - - public static LimitRateInterceptor forRequester(int highTide, int lowTide) { - return new LimitRateInterceptor(true, highTide, lowTide); - } - - public static LimitRateInterceptor forResponder() { - return forRequester(Queues.SMALL_BUFFER_SIZE); - } - - public static LimitRateInterceptor forResponder(int limit) { - return forRequester(limit, limit); - } - - public static LimitRateInterceptor forResponder(int highTide, int lowTide) { - return new LimitRateInterceptor(false, highTide, lowTide); - } - } - - static class LimitRateRSocket extends RSocketProxy { - - final boolean requesterSide; - final int highTide; - final int lowTide; - - public LimitRateRSocket(RSocket source, boolean requesterSide, int highTide, int lowTide) { - super(source); - this.requesterSide = requesterSide; - this.highTide = highTide; - this.lowTide = lowTide; - } - - @Override - public Flux requestStream(Payload payload) { - Flux flux = super.requestStream(payload); - if (requesterSide) { - return flux; - } - return flux.limitRate(highTide, lowTide); - } - - @Override - public Flux requestChannel(Publisher payloads) { - if (requesterSide) { - return super.requestChannel(Flux.from(payloads).limitRate(highTide, lowTide)); - } - return super.requestChannel(payloads).limitRate(highTide, lowTide); - } } } From 08c2a78f25c0b5e78103e95cbf90144cd7ea118a Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 12 May 2020 02:02:43 +0300 Subject: [PATCH 178/181] provides support for TracingMetadata (#824) * provides support for TracingMetadata Signed-off-by: Oleh Dokuka * improves docs Signed-off-by: Oleh Dokuka --- .../io/rsocket/metadata/TracingMetadata.java | 110 +++++++++ .../metadata/TracingMetadataCodec.java | 172 ++++++++++++++ .../metadata/TracingMetadataCodecTest.java | 209 ++++++++++++++++++ 3 files changed, 491 insertions(+) create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadata.java create mode 100644 rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadataCodec.java create mode 100644 rsocket-core/src/test/java/io/rsocket/metadata/TracingMetadataCodecTest.java diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadata.java b/rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadata.java new file mode 100644 index 000000000..d276a9436 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadata.java @@ -0,0 +1,110 @@ +/* + * 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.metadata; + +/** + * Represents decoded tracing metadata which is fully compatible with Zipkin B3 propagation + * + * @since 1.0 + */ +public final class TracingMetadata { + + final long traceIdHigh; + final long traceId; + private final boolean hasParentId; + final long parentId; + final long spanId; + final boolean isEmpty; + final boolean isNotSampled; + final boolean isSampled; + final boolean isDebug; + + TracingMetadata( + long traceIdHigh, + long traceId, + long spanId, + boolean hasParentId, + long parentId, + boolean isEmpty, + boolean isNotSampled, + boolean isSampled, + boolean isDebug) { + this.traceIdHigh = traceIdHigh; + this.traceId = traceId; + this.spanId = spanId; + this.hasParentId = hasParentId; + this.parentId = parentId; + this.isEmpty = isEmpty; + this.isNotSampled = isNotSampled; + this.isSampled = isSampled; + this.isDebug = isDebug; + } + + /** When non-zero, the trace containing this span uses 128-bit trace identifiers. */ + public long traceIdHigh() { + return traceIdHigh; + } + + /** Unique 8-byte identifier for a trace, set on all spans within it. */ + public long traceId() { + return traceId; + } + + /** Indicates if the parent's {@link #spanId} or if this the root span in a trace. */ + public final boolean hasParent() { + return hasParentId; + } + + /** Returns the parent's {@link #spanId} where zero implies absent. */ + public long parentId() { + return parentId; + } + + /** + * Unique 8-byte identifier of this span within a trace. + * + *

    A span is uniquely identified in storage by ({@linkplain #traceId}, {@linkplain #spanId}). + */ + public long spanId() { + return spanId; + } + + /** Indicates that trace IDs should be accepted for tracing. */ + public boolean isSampled() { + return isSampled; + } + + /** Indicates that trace IDs should be force traced. */ + public boolean isDebug() { + return isDebug; + } + + /** Includes that there is sampling information and no trace IDs. */ + public boolean isEmpty() { + return isEmpty; + } + + /** + * Indicated that sampling decision is present. If {@code false} means that decision is unknown + * and says explicitly that {@link #isDebug()} and {@link #isSampled()} also returns {@code + * false}. + */ + public boolean isDecided() { + return isNotSampled || isDebug || isSampled; + } +} diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadataCodec.java b/rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadataCodec.java new file mode 100644 index 000000000..eb44956f6 --- /dev/null +++ b/rsocket-core/src/main/java/io/rsocket/metadata/TracingMetadataCodec.java @@ -0,0 +1,172 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; + +/** + * Represents codes for tracing metadata which is fully compatible with Zipkin B3 propagation + * + * @since 1.0 + */ +public class TracingMetadataCodec { + + static final int FLAG_EXTENDED_TRACE_ID_SIZE = 0b0000_1000; + static final int FLAG_INCLUDE_PARENT_ID = 0b0000_0100; + static final int FLAG_NOT_SAMPLED = 0b0001_0000; + static final int FLAG_SAMPLED = 0b0010_0000; + static final int FLAG_DEBUG = 0b0100_0000; + static final int FLAG_IDS_SET = 0b1000_0000; + + public static ByteBuf encodeEmpty(ByteBufAllocator allocator, Flags flag) { + + return encode(allocator, true, 0, 0, false, 0, 0, false, flag); + } + + public static ByteBuf encode128( + ByteBufAllocator allocator, + long traceIdHigh, + long traceId, + long spanId, + long parentId, + Flags flag) { + + return encode(allocator, false, traceIdHigh, traceId, true, spanId, parentId, true, flag); + } + + public static ByteBuf encode128( + ByteBufAllocator allocator, long traceIdHigh, long traceId, long spanId, Flags flag) { + + return encode(allocator, false, traceIdHigh, traceId, true, spanId, 0, false, flag); + } + + public static ByteBuf encode64( + ByteBufAllocator allocator, long traceId, long spanId, long parentId, Flags flag) { + + return encode(allocator, false, 0, traceId, false, spanId, parentId, true, flag); + } + + public static ByteBuf encode64( + ByteBufAllocator allocator, long traceId, long spanId, Flags flag) { + return encode(allocator, false, 0, traceId, false, spanId, 0, false, flag); + } + + static ByteBuf encode( + ByteBufAllocator allocator, + boolean isEmpty, + long traceIdHigh, + long traceId, + boolean extendedTraceId, + long spanId, + long parentId, + boolean includesParent, + Flags flag) { + int size = + 1 + + (isEmpty + ? 0 + : (Long.BYTES + + Long.BYTES + + (extendedTraceId ? Long.BYTES : 0) + + (includesParent ? Long.BYTES : 0))); + final ByteBuf buffer = allocator.buffer(size); + + int byteFlags = 0; + switch (flag) { + case NOT_SAMPLE: + byteFlags |= FLAG_NOT_SAMPLED; + break; + case SAMPLE: + byteFlags |= FLAG_SAMPLED; + break; + case DEBUG: + byteFlags |= FLAG_DEBUG; + break; + } + + if (isEmpty) { + return buffer.writeByte(byteFlags); + } + + byteFlags |= FLAG_IDS_SET; + + if (extendedTraceId) { + byteFlags |= FLAG_EXTENDED_TRACE_ID_SIZE; + } + + if (includesParent) { + byteFlags |= FLAG_INCLUDE_PARENT_ID; + } + + buffer.writeByte(byteFlags); + + if (extendedTraceId) { + buffer.writeLong(traceIdHigh); + } + + buffer.writeLong(traceId).writeLong(spanId); + + if (includesParent) { + buffer.writeLong(parentId); + } + + return buffer; + } + + public static TracingMetadata decode(ByteBuf byteBuf) { + byteBuf.markReaderIndex(); + try { + byte flags = byteBuf.readByte(); + boolean isNotSampled = (flags & FLAG_NOT_SAMPLED) == FLAG_NOT_SAMPLED; + boolean isSampled = (flags & FLAG_SAMPLED) == FLAG_SAMPLED; + boolean isDebug = (flags & FLAG_DEBUG) == FLAG_DEBUG; + boolean isIDSet = (flags & FLAG_IDS_SET) == FLAG_IDS_SET; + + if (!isIDSet) { + return new TracingMetadata(0, 0, 0, false, 0, true, isNotSampled, isSampled, isDebug); + } + + boolean extendedTraceId = + (flags & FLAG_EXTENDED_TRACE_ID_SIZE) == FLAG_EXTENDED_TRACE_ID_SIZE; + + long traceIdHigh; + if (extendedTraceId) { + traceIdHigh = byteBuf.readLong(); + } else { + traceIdHigh = 0; + } + + long traceId = byteBuf.readLong(); + long spanId = byteBuf.readLong(); + + boolean includesParent = (flags & FLAG_INCLUDE_PARENT_ID) == FLAG_INCLUDE_PARENT_ID; + + long parentId; + if (includesParent) { + parentId = byteBuf.readLong(); + } else { + parentId = 0; + } + + return new TracingMetadata( + traceIdHigh, + traceId, + spanId, + includesParent, + parentId, + false, + isNotSampled, + isSampled, + isDebug); + } finally { + byteBuf.resetReaderIndex(); + } + } + + public enum Flags { + UNDECIDED, + NOT_SAMPLE, + SAMPLE, + DEBUG + } +} diff --git a/rsocket-core/src/test/java/io/rsocket/metadata/TracingMetadataCodecTest.java b/rsocket-core/src/test/java/io/rsocket/metadata/TracingMetadataCodecTest.java new file mode 100644 index 000000000..cb8478c13 --- /dev/null +++ b/rsocket-core/src/test/java/io/rsocket/metadata/TracingMetadataCodecTest.java @@ -0,0 +1,209 @@ +package io.rsocket.metadata; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufAllocator; +import io.netty.util.ReferenceCounted; +import io.rsocket.buffer.LeaksTrackingByteBufAllocator; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Stream; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +public class TracingMetadataCodecTest { + + private static Stream flags() { + return Stream.of(TracingMetadataCodec.Flags.values()); + } + + @ParameterizedTest + @MethodSource("flags") + public void shouldEncodeEmptyTrace(TracingMetadataCodec.Flags expectedFlag) { + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + ByteBuf byteBuf = TracingMetadataCodec.encodeEmpty(allocator, expectedFlag); + + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(byteBuf); + + Assertions.assertThat(tracingMetadata) + .matches(TracingMetadata::isEmpty) + .matches( + tm -> { + switch (expectedFlag) { + case UNDECIDED: + return !tm.isDecided(); + case NOT_SAMPLE: + return tm.isDecided() && !tm.isSampled(); + case SAMPLE: + return tm.isDecided() && tm.isSampled(); + case DEBUG: + return tm.isDecided() && tm.isDebug(); + } + return false; + }); + Assertions.assertThat(byteBuf).matches(ReferenceCounted::release); + allocator.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("flags") + public void shouldEncodeTrace64WithParent(TracingMetadataCodec.Flags expectedFlag) { + long traceId = ThreadLocalRandom.current().nextLong(); + long spanId = ThreadLocalRandom.current().nextLong(); + long parentId = ThreadLocalRandom.current().nextLong(); + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + ByteBuf byteBuf = + TracingMetadataCodec.encode64(allocator, traceId, spanId, parentId, expectedFlag); + + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(byteBuf); + + Assertions.assertThat(tracingMetadata) + .matches(metadata -> !metadata.isEmpty()) + .matches(tm -> tm.traceIdHigh() == 0) + .matches(tm -> tm.traceId() == traceId) + .matches(tm -> tm.spanId() == spanId) + .matches(tm -> tm.hasParent()) + .matches(tm -> tm.parentId() == parentId) + .matches( + tm -> { + switch (expectedFlag) { + case UNDECIDED: + return !tm.isDecided(); + case NOT_SAMPLE: + return tm.isDecided() && !tm.isSampled(); + case SAMPLE: + return tm.isDecided() && tm.isSampled(); + case DEBUG: + return tm.isDecided() && tm.isDebug(); + } + return false; + }); + Assertions.assertThat(byteBuf).matches(ReferenceCounted::release); + allocator.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("flags") + public void shouldEncodeTrace64(TracingMetadataCodec.Flags expectedFlag) { + long traceId = ThreadLocalRandom.current().nextLong(); + long spanId = ThreadLocalRandom.current().nextLong(); + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + ByteBuf byteBuf = TracingMetadataCodec.encode64(allocator, traceId, spanId, expectedFlag); + + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(byteBuf); + + Assertions.assertThat(tracingMetadata) + .matches(metadata -> !metadata.isEmpty()) + .matches(tm -> tm.traceIdHigh() == 0) + .matches(tm -> tm.traceId() == traceId) + .matches(tm -> tm.spanId() == spanId) + .matches(tm -> !tm.hasParent()) + .matches(tm -> tm.parentId() == 0) + .matches( + tm -> { + switch (expectedFlag) { + case UNDECIDED: + return !tm.isDecided(); + case NOT_SAMPLE: + return tm.isDecided() && !tm.isSampled(); + case SAMPLE: + return tm.isDecided() && tm.isSampled(); + case DEBUG: + return tm.isDecided() && tm.isDebug(); + } + return false; + }); + Assertions.assertThat(byteBuf).matches(ReferenceCounted::release); + allocator.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("flags") + public void shouldEncodeTrace128WithParent(TracingMetadataCodec.Flags expectedFlag) { + long traceIdHighLocal; + do { + traceIdHighLocal = ThreadLocalRandom.current().nextLong(); + + } while (traceIdHighLocal == 0); + long traceIdHigh = traceIdHighLocal; + long traceId = ThreadLocalRandom.current().nextLong(); + long spanId = ThreadLocalRandom.current().nextLong(); + long parentId = ThreadLocalRandom.current().nextLong(); + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + ByteBuf byteBuf = + TracingMetadataCodec.encode128( + allocator, traceIdHigh, traceId, spanId, parentId, expectedFlag); + + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(byteBuf); + + Assertions.assertThat(tracingMetadata) + .matches(metadata -> !metadata.isEmpty()) + .matches(tm -> tm.traceIdHigh() == traceIdHigh) + .matches(tm -> tm.traceId() == traceId) + .matches(tm -> tm.spanId() == spanId) + .matches(tm -> tm.hasParent()) + .matches(tm -> tm.parentId() == parentId) + .matches( + tm -> { + switch (expectedFlag) { + case UNDECIDED: + return !tm.isDecided(); + case NOT_SAMPLE: + return tm.isDecided() && !tm.isSampled(); + case SAMPLE: + return tm.isDecided() && tm.isSampled(); + case DEBUG: + return tm.isDecided() && tm.isDebug(); + } + return false; + }); + Assertions.assertThat(byteBuf).matches(ReferenceCounted::release); + allocator.assertHasNoLeaks(); + } + + @ParameterizedTest + @MethodSource("flags") + public void shouldEncodeTrace128(TracingMetadataCodec.Flags expectedFlag) { + long traceIdHighLocal; + do { + traceIdHighLocal = ThreadLocalRandom.current().nextLong(); + + } while (traceIdHighLocal == 0); + long traceIdHigh = traceIdHighLocal; + long traceId = ThreadLocalRandom.current().nextLong(); + long spanId = ThreadLocalRandom.current().nextLong(); + LeaksTrackingByteBufAllocator allocator = + LeaksTrackingByteBufAllocator.instrument(ByteBufAllocator.DEFAULT); + ByteBuf byteBuf = + TracingMetadataCodec.encode128(allocator, traceIdHigh, traceId, spanId, expectedFlag); + + TracingMetadata tracingMetadata = TracingMetadataCodec.decode(byteBuf); + + Assertions.assertThat(tracingMetadata) + .matches(metadata -> !metadata.isEmpty()) + .matches(tm -> tm.traceIdHigh() == traceIdHigh) + .matches(tm -> tm.traceId() == traceId) + .matches(tm -> tm.spanId() == spanId) + .matches(tm -> !tm.hasParent()) + .matches(tm -> tm.parentId() == 0) + .matches( + tm -> { + switch (expectedFlag) { + case UNDECIDED: + return !tm.isDecided(); + case NOT_SAMPLE: + return tm.isDecided() && !tm.isSampled(); + case SAMPLE: + return tm.isDecided() && tm.isSampled(); + case DEBUG: + return tm.isDecided() && tm.isDebug(); + } + return false; + }); + Assertions.assertThat(byteBuf).matches(ReferenceCounted::release); + allocator.assertHasNoLeaks(); + } +} From a9e2954c168e6923836166da326abdc6119eb5c3 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 12 May 2020 09:46:23 +0300 Subject: [PATCH 179/181] bumps version to 1.0.0 Signed-off-by: Oleh Dokuka --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 2e429e05a..c5c4fab15 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0-RC8 +version=1.0.0 perfBaselineVersion=1.0.0-RC7 From 8cb9e747e9b307a3c15312e460c25a8cbdb57ce5 Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 12 May 2020 09:54:56 +0300 Subject: [PATCH 180/181] fixes AuthMetadataCodec methods names Signed-off-by: Oleh Dokuka --- .../rsocket/metadata/AuthMetadataCodec.java | 22 +++++++++---------- .../security/AuthMetadataFlyweight.java | 16 +++++++------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java b/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java index 41dafb33d..d908abb3c 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/AuthMetadataCodec.java @@ -174,7 +174,7 @@ public static boolean isWellKnownAuthType(ByteBuf metadata) { * field's value is length or unknown auth type * @throws IllegalStateException if not enough readable bytes in the given {@link ByteBuf} */ - public static WellKnownAuthType decodeWellKnownAuthType(ByteBuf metadata) { + public static WellKnownAuthType readWellKnownAuthType(ByteBuf metadata) { if (metadata.readableBytes() < 1) { throw new IllegalStateException( "Unable to decode Well Know Auth type. Not enough readable bytes"); @@ -195,7 +195,7 @@ public static WellKnownAuthType decodeWellKnownAuthType(ByteBuf metadata) { * @param metadata * @return */ - public static CharSequence decodeCustomAuthType(ByteBuf metadata) { + public static CharSequence readCustomAuthType(ByteBuf metadata) { if (metadata.readableBytes() < 2) { throw new IllegalStateException( "Unable to decode custom Auth type. Not enough readable bytes"); @@ -226,7 +226,7 @@ public static CharSequence decodeCustomAuthType(ByteBuf metadata) { * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if no bytes readable in the * given one */ - public static ByteBuf decodePayload(ByteBuf metadata) { + public static ByteBuf readPayload(ByteBuf metadata) { if (metadata.readableBytes() == 0) { return Unpooled.EMPTY_BUFFER; } @@ -242,8 +242,8 @@ public static ByteBuf decodePayload(ByteBuf metadata) { * simpleAuthMetadata#readIndex} should be set to the username length byte * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if username length is zero */ - public static ByteBuf decodeUsername(ByteBuf simpleAuthMetadata) { - short usernameLength = decodeUsernameLength(simpleAuthMetadata); + public static ByteBuf readUsername(ByteBuf simpleAuthMetadata) { + short usernameLength = readUsernameLength(simpleAuthMetadata); if (usernameLength == 0) { return Unpooled.EMPTY_BUFFER; @@ -260,7 +260,7 @@ public static ByteBuf decodeUsername(ByteBuf simpleAuthMetadata) { * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if password length is zero */ - public static ByteBuf decodePassword(ByteBuf simpleAuthMetadata) { + public static ByteBuf readPassword(ByteBuf simpleAuthMetadata) { if (simpleAuthMetadata.readableBytes() == 0) { return Unpooled.EMPTY_BUFFER; } @@ -275,8 +275,8 @@ public static ByteBuf decodePassword(ByteBuf simpleAuthMetadata) { * simpleAuthMetadata#readIndex} should be set to the username length byte * @return {@code char[]} which represents UTF-8 username */ - public static char[] decodeUsernameAsCharArray(ByteBuf simpleAuthMetadata) { - short usernameLength = decodeUsernameLength(simpleAuthMetadata); + public static char[] readUsernameAsCharArray(ByteBuf simpleAuthMetadata) { + short usernameLength = readUsernameLength(simpleAuthMetadata); if (usernameLength == 0) { return EMPTY_CHARS_ARRAY; @@ -293,7 +293,7 @@ public static char[] decodeUsernameAsCharArray(ByteBuf simpleAuthMetadata) { * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes * @return {@code char[]} which represents UTF-8 password */ - public static char[] decodePasswordAsCharArray(ByteBuf simpleAuthMetadata) { + public static char[] readPasswordAsCharArray(ByteBuf simpleAuthMetadata) { if (simpleAuthMetadata.readableBytes() == 0) { return EMPTY_CHARS_ARRAY; } @@ -309,7 +309,7 @@ public static char[] decodePasswordAsCharArray(ByteBuf simpleAuthMetadata) { * simpleAuthMetadata#readIndex} should be set to the beginning of the password bytes * @return {@code char[]} which represents UTF-8 password */ - public static char[] decodeBearerTokenAsCharArray(ByteBuf bearerAuthMetadata) { + public static char[] readBearerTokenAsCharArray(ByteBuf bearerAuthMetadata) { if (bearerAuthMetadata.readableBytes() == 0) { return EMPTY_CHARS_ARRAY; } @@ -317,7 +317,7 @@ public static char[] decodeBearerTokenAsCharArray(ByteBuf bearerAuthMetadata) { return CharByteBufUtil.readUtf8(bearerAuthMetadata, bearerAuthMetadata.readableBytes()); } - private static short decodeUsernameLength(ByteBuf simpleAuthMetadata) { + private static short readUsernameLength(ByteBuf simpleAuthMetadata) { if (simpleAuthMetadata.readableBytes() < 1) { throw new IllegalStateException( "Unable to decode custom username. Not enough readable bytes"); diff --git a/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java b/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java index fd990d273..e1a8ba449 100644 --- a/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java +++ b/rsocket-core/src/main/java/io/rsocket/metadata/security/AuthMetadataFlyweight.java @@ -107,7 +107,7 @@ public static boolean isWellKnownAuthType(ByteBuf metadata) { * @throws IllegalStateException if not enough readable bytes in the given {@link ByteBuf} */ public static WellKnownAuthType decodeWellKnownAuthType(ByteBuf metadata) { - return WellKnownAuthType.cast(AuthMetadataCodec.decodeWellKnownAuthType(metadata)); + return WellKnownAuthType.cast(AuthMetadataCodec.readWellKnownAuthType(metadata)); } /** @@ -117,7 +117,7 @@ public static WellKnownAuthType decodeWellKnownAuthType(ByteBuf metadata) { * @return */ public static CharSequence decodeCustomAuthType(ByteBuf metadata) { - return AuthMetadataCodec.decodeCustomAuthType(metadata); + return AuthMetadataCodec.readCustomAuthType(metadata); } /** @@ -130,7 +130,7 @@ public static CharSequence decodeCustomAuthType(ByteBuf metadata) { * given one */ public static ByteBuf decodePayload(ByteBuf metadata) { - return AuthMetadataCodec.decodePayload(metadata); + return AuthMetadataCodec.readPayload(metadata); } /** @@ -142,7 +142,7 @@ public static ByteBuf decodePayload(ByteBuf metadata) { * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if username length is zero */ public static ByteBuf decodeUsername(ByteBuf simpleAuthMetadata) { - return AuthMetadataCodec.decodeUsername(simpleAuthMetadata); + return AuthMetadataCodec.readUsername(simpleAuthMetadata); } /** @@ -154,7 +154,7 @@ public static ByteBuf decodeUsername(ByteBuf simpleAuthMetadata) { * @return sliced {@link ByteBuf} or {@link Unpooled#EMPTY_BUFFER} if password length is zero */ public static ByteBuf decodePassword(ByteBuf simpleAuthMetadata) { - return AuthMetadataCodec.decodePassword(simpleAuthMetadata); + return AuthMetadataCodec.readPassword(simpleAuthMetadata); } /** * Read up to 257 {@code bytes} from the given {@link ByteBuf} where the first byte is username @@ -165,7 +165,7 @@ public static ByteBuf decodePassword(ByteBuf simpleAuthMetadata) { * @return {@code char[]} which represents UTF-8 username */ public static char[] decodeUsernameAsCharArray(ByteBuf simpleAuthMetadata) { - return AuthMetadataCodec.decodeUsernameAsCharArray(simpleAuthMetadata); + return AuthMetadataCodec.readUsernameAsCharArray(simpleAuthMetadata); } /** @@ -177,7 +177,7 @@ public static char[] decodeUsernameAsCharArray(ByteBuf simpleAuthMetadata) { * @return {@code char[]} which represents UTF-8 password */ public static char[] decodePasswordAsCharArray(ByteBuf simpleAuthMetadata) { - return AuthMetadataCodec.decodePasswordAsCharArray(simpleAuthMetadata); + return AuthMetadataCodec.readPasswordAsCharArray(simpleAuthMetadata); } /** @@ -189,6 +189,6 @@ public static char[] decodePasswordAsCharArray(ByteBuf simpleAuthMetadata) { * @return {@code char[]} which represents UTF-8 password */ public static char[] decodeBearerTokenAsCharArray(ByteBuf bearerAuthMetadata) { - return AuthMetadataCodec.decodeBearerTokenAsCharArray(bearerAuthMetadata); + return AuthMetadataCodec.readBearerTokenAsCharArray(bearerAuthMetadata); } } From d903e9635a159285b6943ea93156c31aa406ba5d Mon Sep 17 00:00:00 2001 From: Oleh Dokuka Date: Tue, 12 May 2020 16:14:30 +0300 Subject: [PATCH 181/181] bamps versions Signed-off-by: Oleh Dokuka --- README.md | 8 ++-- .../frame/FrameHeaderFlyweightPerf.java | 10 ++-- .../rsocket/frame/PayloadFlyweightPerf.java | 8 ++-- .../UnicastVsDefaultMonoProcessorPerf.java | 46 ------------------- gradle.properties | 4 +- 5 files changed, 15 insertions(+), 61 deletions(-) delete mode 100644 benchmarks/src/main/java/io/rsocket/internal/UnicastVsDefaultMonoProcessorPerf.java diff --git a/README.md b/README.md index 8a2ba991c..f8110a31e 100644 --- a/README.md +++ b/README.md @@ -26,8 +26,8 @@ repositories { mavenCentral() } dependencies { - implementation 'io.rsocket:rsocket-core:1.0.0-RC7' - implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC7' + implementation 'io.rsocket:rsocket-core:1.0.0' + implementation 'io.rsocket:rsocket-transport-netty:1.0.0' } ``` @@ -40,8 +40,8 @@ repositories { maven { url 'https://oss.jfrog.org/oss-snapshot-local' } } dependencies { - implementation 'io.rsocket:rsocket-core:1.0.0-RC8-SNAPSHOT' - implementation 'io.rsocket:rsocket-transport-netty:1.0.0-RC8-SNAPSHOT' + implementation 'io.rsocket:rsocket-core:1.0.1-SNAPSHOT' + implementation 'io.rsocket:rsocket-transport-netty:1.0.1-SNAPSHOT' } ``` diff --git a/benchmarks/src/main/java/io/rsocket/frame/FrameHeaderFlyweightPerf.java b/benchmarks/src/main/java/io/rsocket/frame/FrameHeaderFlyweightPerf.java index 139114466..b4ac808d0 100644 --- a/benchmarks/src/main/java/io/rsocket/frame/FrameHeaderFlyweightPerf.java +++ b/benchmarks/src/main/java/io/rsocket/frame/FrameHeaderFlyweightPerf.java @@ -16,7 +16,7 @@ public class FrameHeaderFlyweightPerf { @Benchmark public void encode(Input input) { - ByteBuf byteBuf = FrameHeaderFlyweight.encodeStreamZero(input.allocator, FrameType.SETUP, 0); + ByteBuf byteBuf = FrameHeaderCodec.encodeStreamZero(input.allocator, FrameType.SETUP, 0); boolean release = byteBuf.release(); input.bh.consume(release); } @@ -24,9 +24,9 @@ public void encode(Input input) { @Benchmark public void decode(Input input) { ByteBuf frame = input.frame; - FrameType frameType = FrameHeaderFlyweight.frameType(frame); - int streamId = FrameHeaderFlyweight.streamId(frame); - int flags = FrameHeaderFlyweight.flags(frame); + FrameType frameType = FrameHeaderCodec.frameType(frame); + int streamId = FrameHeaderCodec.streamId(frame); + int flags = FrameHeaderCodec.flags(frame); input.bh.consume(streamId); input.bh.consume(flags); input.bh.consume(frameType); @@ -44,7 +44,7 @@ public void setup(Blackhole bh) { this.bh = bh; this.frameType = FrameType.REQUEST_RESPONSE; allocator = ByteBufAllocator.DEFAULT; - frame = FrameHeaderFlyweight.encode(allocator, 123, FrameType.SETUP, 0); + frame = FrameHeaderCodec.encode(allocator, 123, FrameType.SETUP, 0); } @TearDown diff --git a/benchmarks/src/main/java/io/rsocket/frame/PayloadFlyweightPerf.java b/benchmarks/src/main/java/io/rsocket/frame/PayloadFlyweightPerf.java index 89e50a6e9..01d82a08f 100644 --- a/benchmarks/src/main/java/io/rsocket/frame/PayloadFlyweightPerf.java +++ b/benchmarks/src/main/java/io/rsocket/frame/PayloadFlyweightPerf.java @@ -18,7 +18,7 @@ public class PayloadFlyweightPerf { @Benchmark public void encode(Input input) { ByteBuf encode = - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( input.allocator, 100, false, @@ -33,8 +33,8 @@ public void encode(Input input) { @Benchmark public void decode(Input input) { ByteBuf frame = input.payload; - ByteBuf data = PayloadFrameFlyweight.data(frame); - ByteBuf metadata = PayloadFrameFlyweight.metadata(frame); + ByteBuf data = PayloadFrameCodec.data(frame); + ByteBuf metadata = PayloadFrameCodec.metadata(frame); input.bh.consume(data); input.bh.consume(metadata); } @@ -57,7 +57,7 @@ public void setup(Blackhole bh) { // Encode a payload and then copy it a single bytebuf payload = allocator.buffer(); ByteBuf encode = - PayloadFrameFlyweight.encode( + PayloadFrameCodec.encode( allocator, 100, false, diff --git a/benchmarks/src/main/java/io/rsocket/internal/UnicastVsDefaultMonoProcessorPerf.java b/benchmarks/src/main/java/io/rsocket/internal/UnicastVsDefaultMonoProcessorPerf.java deleted file mode 100644 index 963067ef0..000000000 --- a/benchmarks/src/main/java/io/rsocket/internal/UnicastVsDefaultMonoProcessorPerf.java +++ /dev/null @@ -1,46 +0,0 @@ -package io.rsocket.internal; - -import io.rsocket.MaxPerfSubscriber; -import java.util.concurrent.TimeUnit; -import org.openjdk.jmh.annotations.Benchmark; -import org.openjdk.jmh.annotations.BenchmarkMode; -import org.openjdk.jmh.annotations.Fork; -import org.openjdk.jmh.annotations.Measurement; -import org.openjdk.jmh.annotations.Mode; -import org.openjdk.jmh.annotations.OutputTimeUnit; -import org.openjdk.jmh.annotations.Scope; -import org.openjdk.jmh.annotations.State; -import org.openjdk.jmh.annotations.Warmup; -import org.openjdk.jmh.infra.Blackhole; -import reactor.core.publisher.MonoProcessor; - -@BenchmarkMode({Mode.Throughput, Mode.SampleTime}) -@Fork(1) -@Warmup(iterations = 10) -@Measurement(iterations = 10, time = 20) -@State(Scope.Benchmark) -@OutputTimeUnit(TimeUnit.MICROSECONDS) -public class UnicastVsDefaultMonoProcessorPerf { - - @Benchmark - public void monoProcessorPerf(Blackhole bh) { - MaxPerfSubscriber subscriber = new MaxPerfSubscriber<>(bh); - MonoProcessor monoProcessor = MonoProcessor.create(); - monoProcessor.onNext(1); - monoProcessor.subscribe(subscriber); - - bh.consume(monoProcessor); - bh.consume(subscriber); - } - - @Benchmark - public void unicastMonoProcessorPerf(Blackhole bh) { - MaxPerfSubscriber subscriber = new MaxPerfSubscriber<>(bh); - UnicastMonoProcessor monoProcessor = UnicastMonoProcessor.create(); - monoProcessor.onNext(1); - monoProcessor.subscribe(subscriber); - - bh.consume(monoProcessor); - bh.consume(subscriber); - } -} diff --git a/gradle.properties b/gradle.properties index c5c4fab15..b0b107ec4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,5 @@ # See the License for the specific language governing permissions and # limitations under the License. # -version=1.0.0 -perfBaselineVersion=1.0.0-RC7 +version=1.0.1 +perfBaselineVersion=1.0.0